Livrare LOT 1 - Didi
This commit is contained in:
commit
5380c3fc63
990 changed files with 133308 additions and 0 deletions
496
ai_platform/modules/forensic_features/docs/MODULES.md
Normal file
496
ai_platform/modules/forensic_features/docs/MODULES.md
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
# Module Deep Dive
|
||||
|
||||
Detaliu tehnic pentru fiecare din cele 5 module. Algoritm exact, parametri,
|
||||
câmpuri de output specifice, limitări cunoscute.
|
||||
|
||||
---
|
||||
|
||||
## m25 — Physiology (rPPG + Blink Dynamics)
|
||||
|
||||
**Scop**: detectează semnale fiziologice care nu pot fi falsificate de AI:
|
||||
puls cardiac prin remote photoplethysmography (rPPG) din variația de culoare
|
||||
facială, plus dinamica clipitului ochilor.
|
||||
|
||||
**Fișier**: [tools/m25_physiology/physiology.py](../tools/m25_physiology/physiology.py)
|
||||
|
||||
### Algoritm
|
||||
|
||||
#### Partea 1: rPPG via POS (Plane Orthogonal to Skin, Wang et al. 2017)
|
||||
|
||||
1. **Per cadru**: detectează fața cu MediaPipe FaceMesh (478 landmarks).
|
||||
Extrage ROI obraz stânga + dreapta din landmarks-uri specifice
|
||||
(`LEFT_CHEEK_LM=[101,207,187]`, `RIGHT_CHEEK_LM=[330,427,411]`).
|
||||
Mediază RGB pe fiecare ROI.
|
||||
|
||||
2. **Construiește seria temporală** RGB(t) — câte un sample per cadru.
|
||||
|
||||
3. **POS algorithm**:
|
||||
```
|
||||
C_n(t) = C(t) / mean(C) # normalizare temporală
|
||||
X = 3*R_n - 2*G_n
|
||||
Y = 1.5*R_n + G_n - 1.5*B_n
|
||||
α = std(X) / std(Y)
|
||||
P(t) = X(t) + α * Y(t)
|
||||
```
|
||||
|
||||
4. **Detrending polynomial ordin 3** pe P(t) — elimină drift slow din
|
||||
variația de iluminare (cloud cover, AGC cameră). Critical pentru rPPG
|
||||
fiabil pe video real.
|
||||
|
||||
5. **FFT + bandpass** 0.7-4 Hz (40-240 BPM range fiziologic):
|
||||
- Identifică peak în banda 50-110 BPM
|
||||
- BPM = peak_freq × 60
|
||||
- SNR = power(peak ± 0.2 Hz) / power(restul benzii)
|
||||
|
||||
#### Partea 2: Blink Dynamics
|
||||
|
||||
1. **EAR per ochi** (Eye Aspect Ratio, Soukupová & Čech 2016):
|
||||
```
|
||||
EAR = (||p2-p6|| + ||p3-p5||) / (2 × ||p1-p4||)
|
||||
```
|
||||
Calculat separat pe ochi stâng (landmarks `[33, 160, 158, 133, 153, 144]`)
|
||||
și drept (`[362, 385, 387, 263, 373, 380]`).
|
||||
|
||||
2. **Detectare blink event**: tranziție EAR < 0.20 → EAR > 0.25.
|
||||
Pentru fiecare blink: start_idx, min_idx, end_idx, min_ear.
|
||||
|
||||
3. **Asimetrie L-R temporală**: pentru fiecare blink stâng, găsește perechea
|
||||
cea mai apropiată în timp pe dreapta. Calculează diferența în ms.
|
||||
Real human: 30-80 ms asimetrie (asimetrie neurologică naturală).
|
||||
AI face: simetric perfect sau jitter aleator.
|
||||
|
||||
### Output specific (summary)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"pulse_bpm": 82.4, // BPM detectat (null dacă fără semnal)
|
||||
"pulse_snr_db": 1.1, // SNR în dB (>3 = semnal credibil)
|
||||
"blink_count_left": 1,
|
||||
"blink_count_right": 1,
|
||||
"blink_count_total": 1,
|
||||
"blink_asymmetry_ms": 42.0, // ms diferență temporală L-R
|
||||
"fps_used": 8.0, // fps efectiv (din _meta.json)
|
||||
"rppg_method": "POS",
|
||||
"mediapipe_used": true
|
||||
}
|
||||
```
|
||||
|
||||
### Limitări
|
||||
|
||||
- **Sparse extraction**: dacă fps efectiv < 4, rPPG e imposibil (sub Nyquist
|
||||
pentru banda 0.7-4 Hz). Modulul returnează NO_SIGNAL clar.
|
||||
- **Iluminare variabilă**: face mai dificil detrending-ul. Pe video cu
|
||||
flickering puternic, semnal degradat.
|
||||
- **Față mică** (< 150 px lățime): ROI obraz prea mic pentru sample RGB
|
||||
stabil.
|
||||
- **MediaPipe rateaza fața**: pe ochi închiși, profil oblic >45°, low-light.
|
||||
|
||||
### PNG-uri generate
|
||||
|
||||
- `m25_pulse_signal.png` — grafic P(t) detrendat + spectrul FFT cu peak marcat
|
||||
- `m25_blink_timeline.png` — EAR L și R în timp + zonele de blink evidențiate
|
||||
|
||||
---
|
||||
|
||||
## m26 — Audio Forensics (Lip-Sync + Voice Clone Heuristic)
|
||||
|
||||
**Scop**: analizează coloana sonoră pentru drift între mișcarea buzelor și
|
||||
audio (lip-sync offset) + caracteristici statistice ale vocii sintetice
|
||||
(F0 prea stabil, centroidă spectrală prea constantă, lipsa pauzelor
|
||||
respiratorii).
|
||||
|
||||
**Fișier**: [tools/m26_audio/audio.py](../tools/m26_audio/audio.py)
|
||||
|
||||
### Algoritm
|
||||
|
||||
#### Audio extraction (in-memory, fără disk I/O)
|
||||
|
||||
`ffmpeg -i video.mp4 -vn -ac 1 -ar 16000 -f f32le pipe:1`
|
||||
|
||||
Output: PCM float32 mono 16 kHz în memorie.
|
||||
|
||||
#### VAD (Voice Activity Detection) inline
|
||||
|
||||
Combină energie locală + zero-crossing rate per fereastră 30ms:
|
||||
- Threshold energie adaptiv = 30th percentile × 1.5
|
||||
- Voce = energie > threshold AND ZCR în [0.02, 0.30]
|
||||
- Returnează `voiced` mask + `voice_ratio` global
|
||||
|
||||
Folosit ca **gate** pe restul analizei — analizăm F0/centroid DOAR pe
|
||||
ferestrele unde VAD spune că e voce.
|
||||
|
||||
#### F0 (pitch fundamental) via autocorelație
|
||||
|
||||
Per fereastră 25ms (hop 10ms):
|
||||
- Autocorelație normalizată a semnalului
|
||||
- Caută peak în banda 75-400 Hz (range voce umană)
|
||||
- F0 = sample_rate / peak_lag dacă ac[peak] > 0.3, altfel 0
|
||||
|
||||
#### Spectral centroid
|
||||
|
||||
Per fereastră 25ms cu Hann window:
|
||||
- FFT magnitude spectrum
|
||||
- Centroid = Σ(freq × magnitude) / Σ(magnitude)
|
||||
|
||||
#### Lip-sync offset (cross-correlation)
|
||||
|
||||
1. Pe video, extrage MediaPipe FaceMesh per cadru, calculează:
|
||||
```
|
||||
mouth_aperture = ||lm_13 - lm_14|| / ||lm_61 - lm_291||
|
||||
```
|
||||
(deschidere verticală / lățime gură).
|
||||
|
||||
2. Resample mouth_aperture la 100 Hz (același rate ca audio envelope).
|
||||
|
||||
3. Cross-correlation FFT-based între:
|
||||
- audio RMS envelope (per fereastră 10ms)
|
||||
- mouth_aperture differential (|d_aperture/dt|)
|
||||
|
||||
4. Lag care maximizează corelația = lip_sync_offset_ms.
|
||||
- |offset| < 60 ms → real video
|
||||
- |offset| > 150 ms → deepfake lip-sync (Wav2Lip-class drift)
|
||||
|
||||
### Output specific (summary)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"lip_sync_offset_ms": 187.0, // ms (null dacă fără față)
|
||||
"lip_sync_correlation": 0.42,
|
||||
"f0_std_hz": 12.3, // <25 Hz = TTS-like
|
||||
"spectral_centroid_std_hz": 145.0, // <250 Hz = TTS-like
|
||||
"silence_ratio": 0.05,
|
||||
"voice_ratio": 0.95,
|
||||
"n_voiced_frames": 480,
|
||||
"audio_duration_s": 20.4,
|
||||
"audio_sample_rate": 16000,
|
||||
"lip_sync_component": 0.9,
|
||||
"voice_clone_component": 0.7
|
||||
}
|
||||
```
|
||||
|
||||
### Limitări
|
||||
|
||||
- **Fără audio**: m26 returnează NO_SIGNAL. Orchestrator-ul auto-skip cu
|
||||
ffprobe verificare înainte de execuție.
|
||||
- **Audio cu zgomot puternic**: VAD raporteaza voce când e doar zgomot;
|
||||
F0 e brittle pe semnale noise-y.
|
||||
- **Profil oblic**: mouth_aperture devine instabilă când unghiul depășește
|
||||
30° (raport vertical/horiz inflate).
|
||||
- **Voice clone heuristic e PROXY**: pentru detector real, integrează
|
||||
AASIST sau RawNet2.
|
||||
|
||||
### PNG-uri generate
|
||||
|
||||
- `m26_audio_visual_sync.png` — overlay audio envelope normalizat + mouth
|
||||
aperture, cu lag marcat. Util pentru LLM să vadă vizual decalajul.
|
||||
|
||||
---
|
||||
|
||||
## m27 — AI-Generated Image Detector (Black-Box)
|
||||
|
||||
**Scop**: distinge imagini fotografice naturale de imagini generate AI
|
||||
(GAN, diffusion models).
|
||||
|
||||
**Fișier**: [tools/m27_ai_detector/ai_detector.py](../tools/m27_ai_detector/ai_detector.py)
|
||||
|
||||
### Algoritm
|
||||
|
||||
Trei semnale combinate:
|
||||
|
||||
#### NPR (Neighboring Pixel Relationships, Tan et al. 2024 — adapted)
|
||||
|
||||
1. Construiește **piramida Gaussian** 4 niveluri din imaginea grayscale.
|
||||
2. Resize toate nivelurile la dimensiunea originală.
|
||||
3. Pentru fiecare pereche de niveluri consecutive: calculează **reziduul**
|
||||
și **varianța locală 3×3** a reziduului².
|
||||
4. Medianul varianțelor → `mean_residual_var`.
|
||||
5. **Sigmoid centrat pe 3.5** → score 0..1 (mai mic mean → mai suspect AI).
|
||||
|
||||
Real video natural: mean_residual_var ~5-30 (textură fluctuantă).
|
||||
AI generated: mean_residual_var ~0.5-4 (smooth predict).
|
||||
|
||||
#### JPEG Reconstruction Error (in-memory)
|
||||
|
||||
1. `cv2.imencode(".jpg", frame, [JPEG_QUALITY, 65])`
|
||||
2. `cv2.imdecode(buf)` — recompressed
|
||||
3. `error_norm = mean(|orig - recomp|) / std(orig)`
|
||||
4. **Sigmoid centrat pe 0.04** (steep) → score 0..1.
|
||||
|
||||
Imagini cu detalii naturale → error_norm mare (~0.10-0.15).
|
||||
AI imagery → error_norm mic (~0.02-0.06).
|
||||
|
||||
#### HuggingFace Detector (OPT-IN, dezactivat default)
|
||||
|
||||
Dacă `M27_USE_HF=1`:
|
||||
- Încarcă `Organika/sdxl-detector` (ViT base ~330MB) prin transformers
|
||||
- Inferință per cadru, output: score 0..1 = probabilitate AI
|
||||
|
||||
**Defensive fusion**: HF folosit DOAR când e confident extrem (>0.85 sau <0.15).
|
||||
În rest cade pe NPR + JPEG.
|
||||
|
||||
**Status empiric**: testat pe 30 samples reale (TikTok video + AI images).
|
||||
Out-of-distribution masiv → regresie de la 27% strict (fără HF) la 13-20%
|
||||
(cu HF). **NU recomandat pe video data**. Cod prezent pentru când se găsește
|
||||
un model mai bine adaptat distribuției tale.
|
||||
|
||||
### Fusion internă
|
||||
|
||||
Dacă există HF score și e confident:
|
||||
```
|
||||
primary = 0.50 × hf_mean + 0.30 × npr_mean + 0.20 × jpeg_mean
|
||||
```
|
||||
|
||||
Altfel (default):
|
||||
```
|
||||
primary = max(npr_mean, jpeg_mean)
|
||||
```
|
||||
|
||||
### Output specific (summary)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"hf_score_mean": null, // sau float 0..1 dacă activat
|
||||
"hf_model_loaded": false,
|
||||
"hf_model_status": "HF detector disabled (set M27_USE_HF=1)",
|
||||
"npr_score_mean": 0.48,
|
||||
"npr_score_std": 0.10,
|
||||
"jpeg_recon_score_mean": 0.78,
|
||||
"jpeg_recon_score_std": 0.05,
|
||||
"torch_score_mean": null,
|
||||
"torch_model_loaded": false,
|
||||
"n_frames_sampled": 16
|
||||
}
|
||||
```
|
||||
|
||||
### Limitări
|
||||
|
||||
- **Statistical-only fără HF**: discriminative power limitat. Pe video real
|
||||
cu compresie puternică, JPEG-recon dă fals-pozitive (vezi tabel din
|
||||
experimente: pe REAL video tipic 0.70-0.85 FAKE).
|
||||
- **NPR sensibil la rezoluție**: imagini sub 64×64 → fallback la 0.5 neutru.
|
||||
- **Pe video re-encodat**: codec deja a smoothed detalii fine → ambele semnale
|
||||
se confundă cu AI-generated.
|
||||
|
||||
### PNG generat
|
||||
|
||||
- `m27_score_timeline.png` — line plot scoruri NPR + JPEG per cadru sample.
|
||||
|
||||
---
|
||||
|
||||
## m28 — Forgery Localization Heatmap
|
||||
|
||||
**Scop**: produce o **hartă 2D** unde fiecare pixel are probabilitate de
|
||||
manipulare locală. **Cel mai util output pentru LLM** — îi dai imagine PNG
|
||||
cu zona suspect colorată, LLM-ul confirmă vizual.
|
||||
|
||||
**Fișier**: [tools/m28_forgery_heatmap/forgery_heatmap.py](../tools/m28_forgery_heatmap/forgery_heatmap.py)
|
||||
|
||||
### Algoritm (Face X-ray-inspired, simplificat fără rețea)
|
||||
|
||||
1. **Detectează fața** cu MediaPipe FaceMesh, extrage contour din
|
||||
`FACE_OVAL` landmarks. Construiește mască poligonală binară.
|
||||
|
||||
2. **Boundary band adaptiv** la mărimea feței: `max(8, face_width * 0.05)`.
|
||||
|
||||
3. **3 hărți de discontinuitate**:
|
||||
|
||||
**(a) Multi-scale Laplacian discrepancy**:
|
||||
- Aplică Laplacian la scale [3, 7, 15] (Gaussian smoothed)
|
||||
- Stivuiește; pentru fiecare pixel: std cross-scale
|
||||
- Limitează la boundary band
|
||||
|
||||
**(b) Frequency-domain split inconsistency**:
|
||||
- FFT global → high-pass (cutoff 15% rază)
|
||||
- Gradient Sobel pe high-freq map
|
||||
- Limitează la boundary band
|
||||
|
||||
**(c) Chrominance step in LAB**:
|
||||
- Convert LAB; gradient pe canalele a și b
|
||||
- Localizat strict pe boundary band
|
||||
|
||||
4. **Compunere ponderată** (NU max, ca să nu satureze la outlier):
|
||||
```
|
||||
combined = 0.5*map_chroma + 0.3*map_freq + 0.2*map_laplacian
|
||||
heatmap = sigmoid(6 * (combined - 0.5))
|
||||
```
|
||||
|
||||
5. **Peak detection**: bounding box al regiunii cu max suspicion.
|
||||
`boundary_mean_suspicion` = media heatmap pe banda boundary.
|
||||
|
||||
### Output specific (summary)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"peak_suspicion_max": 0.90,
|
||||
"peak_suspicion_mean": 0.88,
|
||||
"boundary_mean_suspicion": 0.14, // metric principal — mai stabil
|
||||
"frames_with_face": 5,
|
||||
"mediapipe_used": true
|
||||
}
|
||||
```
|
||||
|
||||
### Scoring final
|
||||
|
||||
```
|
||||
score = 0.4 * peak_max + 0.6 * min(1.0, (boundary_mean - 0.05) / 0.20)
|
||||
```
|
||||
|
||||
Peak ridicat + boundary_mean mic = noise (un pixel anomalous).
|
||||
Peak ridicat + boundary_mean ridicat = blending real detectat.
|
||||
|
||||
### Limitări
|
||||
|
||||
- **Pe full-AI generated** (fără boundary face-swap), m28 nu vede mare diferență.
|
||||
Pentru asta folosește m27.
|
||||
- **MediaPipe rateaza fața** → fallback la **Haar eliptic** (mai puțin precis).
|
||||
Pe video oblice/low-light, fallback dă fals-pozitive frecvente.
|
||||
- **Boundary band fix vs. mărime față**: adaptiv 5% lățime — funcționează
|
||||
bine, dar pe fețe foarte mici (<80 px) banda devine prea îngustă.
|
||||
|
||||
### PNG generat (cel mai util pentru LLM)
|
||||
|
||||
- `m28_heatmap_{frame_idx}.png` — overlay original + heatmap fierbinte cu
|
||||
zona suspect colorată. Câte unul per 5 frame samples = 5 PNG-uri.
|
||||
|
||||
**LLM-ul vede aceste imagini** și poate spune: "da, văd zona aceea
|
||||
evidențiată — și văd EFECTIV un edge artificial la jawline". Confirmare
|
||||
vizuală + numerică = signal puternic.
|
||||
|
||||
---
|
||||
|
||||
## m29 — Lighting 3D Consistency
|
||||
|
||||
**Scop**: verifică dacă fața și scena sunt iluminate de aceleași surse de
|
||||
lumină. Mismatch unghi = subiect compus.
|
||||
|
||||
**Fișier**: [tools/m29_lighting/lighting.py](../tools/m29_lighting/lighting.py)
|
||||
|
||||
### Algoritm
|
||||
|
||||
#### Estimare direcție lumină față (Lambertian SfS simplificat)
|
||||
|
||||
1. **Aproximare normale 3D ca sferă**: centrul = centroid landmark-uri,
|
||||
rază = jumătate lățime față.
|
||||
```
|
||||
N(x, y) = ((x-cx)/r, (y-cy)/r, sqrt(1 - dx² - dy²))
|
||||
```
|
||||
|
||||
2. **Lambertian model**:
|
||||
```
|
||||
I(x,y) ≈ ρ * max(N · L, 0) + ambient
|
||||
```
|
||||
|
||||
3. **Least squares fit** pe pixeli mască:
|
||||
```
|
||||
[N | 1] @ [L_x, L_y, L_z, ambient]^T = I
|
||||
```
|
||||
Rezolvă cu `np.linalg.lstsq`.
|
||||
|
||||
4. **Normalizare** și conversie sferică:
|
||||
- azimuth = atan2(L_x, L_z)
|
||||
- elevation = asin(-L_y)
|
||||
|
||||
#### Estimare direcție lumină scenă
|
||||
|
||||
1. **Highlight detection** în zona NON-face:
|
||||
- `V > percentila 95` în HSV
|
||||
- `S < 80` (saturație scăzută — highlights sunt aproape albe)
|
||||
|
||||
2. **Connected components** pe highlight mask.
|
||||
|
||||
3. **Weighted centroid** (greutate = area cluster):
|
||||
- dx = mediu_x_clusters - W/2
|
||||
- dy = mediu_y_clusters - H/2
|
||||
|
||||
4. Convertit în (azimuth_scene, elevation_scene).
|
||||
|
||||
#### Angular discrepancy
|
||||
|
||||
```
|
||||
angle = acos(L_face · L_scene)
|
||||
```
|
||||
- < 60° → consistent
|
||||
- 60-90° → marginal
|
||||
- > 90° → mismatch suspect
|
||||
|
||||
#### Catchlight consistency
|
||||
|
||||
1. ROI ochi stâng + drept (din landmarks).
|
||||
2. Top 1% intensitate per ochi → centroid + intensitate.
|
||||
3. Comparare: poziții relative trebuie să fie aproximativ **oglindite**
|
||||
(ochiul drept e oglindit pe x).
|
||||
4. Score = 1 - normalize(|dx_diff_mirror| + |dy_diff| + |intensity_diff|).
|
||||
|
||||
### Output specific (summary)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"lighting_mismatch_deg_mean": 95.0,
|
||||
"lighting_mismatch_deg_max": 105.4,
|
||||
"catchlight_consistency_mean": null, // sau float 0..1
|
||||
"frames_with_lighting_signal": 5,
|
||||
"frames_with_catchlights": 0,
|
||||
"lighting_component": 0.76,
|
||||
"catchlight_component": 0.40
|
||||
}
|
||||
```
|
||||
|
||||
### Limitări
|
||||
|
||||
- **Spherical approximation** pentru normale 3D e grosier — pentru SOTA
|
||||
folosește 3DMM (FLAME, BFM2009) fittat real cu eos-py.
|
||||
- **Scene fără highlights**: nu putem estima direcția scenă → NO_SIGNAL.
|
||||
Lumină ambientă uniformă (interior office, cer înnorat) e cazul tipic.
|
||||
- **Catchlight pe ochi mici** (<30×30 px): primul pixel câștigă, semnal zgomot.
|
||||
|
||||
### PNG generat
|
||||
|
||||
- `m29_lighting_{frame_idx}.png` — overlay imagine + 2 săgeți care arată
|
||||
direcția estimată: **roșie** pentru lumină față, **albastră** pentru scenă.
|
||||
Mismatch vizibil instant.
|
||||
|
||||
---
|
||||
|
||||
## Cum să adaugi un modul nou (mXX)
|
||||
|
||||
1. Creează folder `tools/mXX_nume/`:
|
||||
```
|
||||
tools/mXX_nume/
|
||||
├── __init__.py
|
||||
├── nume.json # config: id, input_type, parameters
|
||||
└── nume.py # cod algoritm
|
||||
```
|
||||
|
||||
2. În `nume.py`:
|
||||
```python
|
||||
from tools._contract import make_response, empty_response
|
||||
|
||||
TOOL_ID = "mXX"
|
||||
TOOL_NAME = "Numele tău"
|
||||
VERSION = "1.0"
|
||||
INPUT_TYPE = "overview_frames" # sau "video_path"
|
||||
|
||||
def run(frame_paths, results_dir=None):
|
||||
# ... algoritmul tău ...
|
||||
return make_response(
|
||||
tool_id=TOOL_ID, tool_name=TOOL_NAME,
|
||||
version=VERSION, input_type=INPUT_TYPE,
|
||||
primary_score=0.5, confidence=0.7,
|
||||
evidence=["...", "..."],
|
||||
frames_analyzed=N, frames_with_signal=M,
|
||||
summary_extras={"custom_field": value},
|
||||
artifacts_images=["mXX_visualization.png"],
|
||||
)
|
||||
```
|
||||
|
||||
3. În `forensic/orchestrator.py:AVAILABLE_MODULES`, adaugă:
|
||||
```python
|
||||
"mXX": ("tools.mXX_nume.nume", "run", "overview_frames"),
|
||||
```
|
||||
|
||||
4. În `forensic/scoring.py:DEFAULT_WEIGHTS`, adaugă weight (default 1.0):
|
||||
```python
|
||||
"mXX": 1.0,
|
||||
```
|
||||
|
||||
5. Rebuild container. Modulul apare automat în `/api/forensic-modules`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue