File size: 7,750 Bytes
be0477a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114d2f1
 
 
 
 
 
 
 
 
 
be0477a
1
2
3
4
5
6
7
8
9
10
11
12
13
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
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
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
import sys
from pathlib import Path

# 1. Injeta o caminho ANTES de qualquer import local ou externo
ROOT_DIR = Path(__file__).resolve().parent.parent
CLIP_SURGERY_PATH = str(ROOT_DIR / "CLIP_Surgery")

if CLIP_SURGERY_PATH not in sys.path:
    sys.path.insert(0, CLIP_SURGERY_PATH)

import clip as clip_surgery
import cv2
import os
import mediapipe as mp
from mediapipe.tasks import python
from mediapipe.tasks.python import vision
import numpy as np
import torch
from PIL import Image
from segmenter import FaceSegmenter
from huggingface_hub import hf_hub_download

from torchvision import transforms
from torchvision.transforms import InterpolationMode

from config import (
    CLIP_MEAN,
    CLIP_STD,
    DEVICE,
    FAKE_PROMPT_KEYWORDS,
    REAL_PROMPTS,
    SURGERY_PROMPTS,
    SURGERY_RES,
)

# ════════════════════════════════════════════════════════════
# MEDIAPIPE — Máscara facial (API Tasks)
# ════════════════════════════════════════════════════════════

_landmarker = None
_segmenter_instance = None

def get_segmenter():
    global _segmenter_instance
    if _segmenter_instance is None:
        print(" A instanciar BiSeNet FaceSegmenter...")
        model_path = hf_hub_download(repo_id="liamu/Deepfake-Pesos", filename="79999_iter.pth")
        _segmenter_instance = FaceSegmenter(model_path=model_path, device=DEVICE)
        print(" BiSeNet pronto.")
    return _segmenter_instance

def get_region_masks(img_rgb: np.ndarray) -> dict:
    return get_segmenter().get_masks(img_rgb)

def get_landmarker():
    global _landmarker
    if _landmarker is None:
        # Caminho para o ficheiro .task na raiz
        model_path = hf_hub_download(
                repo_id="liamu/Deepfake-Pesos", 
                filename="face_landmarker.task")
            
        base_options = python.BaseOptions(model_asset_path=model_path)
        options = vision.FaceLandmarkerOptions(
            base_options=base_options,
            output_face_blendshapes=False,
            output_facial_transformation_matrixes=False,
            num_faces=1
        )
        _landmarker = vision.FaceLandmarker.create_from_options(options)
    return _landmarker

def build_face_mask(img_rgb: np.ndarray) -> np.ndarray:
    try:
        h, w = img_rgb.shape[:2]
        if img_rgb is None or img_rgb.size == 0:
            return np.ones((h, w), dtype=np.float32)

        # Converter para formato do MediaPipe
        mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=img_rgb)
        
        landmarker = get_landmarker()
        detection_result = landmarker.detect(mp_image)

        if not detection_result.face_landmarks:
            return np.ones((h, w), dtype=np.float32)

        lm = detection_result.face_landmarks[0]
        points = np.array(
            [(int(point.x * w), int(point.y * h)) for point in lm], dtype=np.int32
        )
        hull = cv2.convexHull(points)

        y_min = hull[:, 0, 1].min()
        y_max = hull[:, 0, 1].max()
        face_h = y_max - y_min

        # 10% padding para cima
        forehead_expansion = int(face_h * 0.10)
        hull_expanded = hull.copy()
        
        top_mask = hull_expanded[:, 0, 1] < (y_min + face_h * 0.35)
        hull_expanded[top_mask, 0, 1] = np.maximum(
            0, hull_expanded[top_mask, 0, 1] - forehead_expansion
        )

        mask = np.zeros((h, w), dtype=np.uint8)
        cv2.fillConvexPoly(mask, hull_expanded, 1)
        
        mask_f = cv2.GaussianBlur(mask.astype(np.float32), (31, 31), 0)
        mask_f = mask_f / (mask_f.max() + 1e-8)
        return mask_f

    except Exception as e:
        print(f"[Aviso] Fallback build_face_mask: {e}")
        return np.ones((h, w), dtype=np.float32)

# ════════════════════════════════════════════════════════════
# CLIP SURGERY — Heatmaps visuais
# ════════════════════════════════════════════════════════════

_surgery_model = None
_surgery_preprocess = None

def get_surgery_model():
    global _surgery_model, _surgery_preprocess
    if _surgery_model is None:
        print(" A carregar CLIP Surgery CS-ViT-L/14...")
        _surgery_model, _ = clip_surgery.load("CS-ViT-L/14", device=DEVICE)
        _surgery_model.eval()
        _surgery_preprocess = transforms.Compose(
            [
                transforms.Resize(
                    (SURGERY_RES, SURGERY_RES),
                    interpolation=InterpolationMode.BICUBIC,
                ),
                transforms.ToTensor(),
                transforms.Normalize(CLIP_MEAN, CLIP_STD),
            ]
        )
        print(" CLIP Surgery pronto.")
    return _surgery_model, _surgery_preprocess

def generate_heatmap(img_rgb, method: str = ""):
    prompts = SURGERY_PROMPTS
    sm, sp = get_surgery_model()
    h, w = img_rgb.shape[:2]
    tensor = sp(Image.fromarray(img_rgb)).unsqueeze(0).to(DEVICE)

    with torch.no_grad():
        img_feats = sm.encode_image(tensor)
        img_feats = img_feats / img_feats.norm(dim=-1, keepdim=True)
        txt_feats = clip_surgery.encode_text_with_prompt_ensemble(sm, prompts, DEVICE)
        similarity = clip_surgery.clip_feature_surgery(img_feats, txt_feats)
        sim_map = clip_surgery.get_similarity_map(similarity[:, 1:, :], (h, w))

    face_mask = build_face_mask(img_rgb)
    face_pixels = face_mask > 0.5
    sim_np = sim_map[0].cpu().numpy()

    fake_maps, real_maps, per_text, scores = [], [], {}, {}

    for n, text in enumerate(prompts):
        m = sim_np[:, :, n]
        m = (m - m.min()) / (m.max() - m.min() + 1e-8)
        m = m * face_mask
        m = (m - m.min()) / (m.max() - m.min() + 1e-8)
        per_text[text] = m.astype(np.float32)
        scores[text] = float(m[face_pixels].mean()) if face_pixels.any() else 0.0

        if any(kw.lower() in text.lower() for kw in FAKE_PROMPT_KEYWORDS):
            fake_maps.append(m)
        elif text in REAL_PROMPTS:
            real_maps.append(m)

    manip_mean = np.mean(fake_maps, axis=0).astype(np.float32) if fake_maps else np.zeros((h, w), dtype=np.float32)
    real_mean = np.mean(real_maps, axis=0).astype(np.float32) if real_maps else np.zeros((h, w), dtype=np.float32)

    contrastive = np.clip(manip_mean - real_mean, 0, None)
    if contrastive.max() > 1e-8:
        contrastive = (contrastive / contrastive.max()).astype(np.float32)
    contrastive = contrastive * face_mask

    manip_scores = {t: s for t, s in scores.items() if any(kw.lower() in t.lower() for kw in FAKE_PROMPT_KEYWORDS)}
    top_prompt_name = max(manip_scores, key=manip_scores.get) if manip_scores else max(scores, key=scores.get)
    top_heatmap = per_text[top_prompt_name]

    return contrastive, per_text, scores, prompts, top_heatmap

def score_regions_manipulation(img_hires, heatmap, masks, scores):
    reg_scores = {}
    h, w = heatmap.shape[:2]
    for name, mask in masks.items():
        mask_resized = cv2.resize(mask, (w, h), interpolation=cv2.INTER_NEAREST)
        pixels = mask_resized > 0
        if not pixels.any():
            reg_scores[name] = {"contrast": 0.0}
            continue

        vals = heatmap[pixels]
        p95    = np.percentile(vals, 95)
        active = float((vals > 0.15).sum()) / (float(pixels.sum()) + 1e-6)
        reg_scores[name] = {"contrast": p95 * active}

    return reg_scores