didi-lot1-ai/ai_platform/modules/forensic_features/docs/MODULES.md

16 KiB
Raw Permalink Blame History

Module Deep Dive

Detaliu tehnic pentru fiecare din cele 5 module. Algoritm exact, parametri, câmpuri de output specifice, limitări cunoscute.


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

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)
  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)

{
  "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

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)

{
  "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

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)

{
  "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

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)

{
  "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

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)

{
  "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:

    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ă:

    "mXX": ("tools.mXX_nume.nume", "run", "overview_frames"),
    
  4. În forensic/scoring.py:DEFAULT_WEIGHTS, adaugă weight (default 1.0):

    "mXX": 1.0,
    
  5. Rebuild container. Modulul apare automat în /api/forensic-modules.