KoreaPeter's picture
Update app.py
2a62dc4 verified
Raw History Blame Contribute Delete
15.6 kB
import spaces
import gradio as gr
import torch
import tempfile
import os
from PIL import Image
from transformers import pipeline
import deepguard # noqa: F401 # registers ms_eff_gcvit_* into timm
from explainability import (
HiResCAMExplainer, GradCAMElementWiseExplainer, LayerCAMExplainer,
EigenGradCAMExplainer, GradCAMPlusPlusExplainer, XGradCAMExplainer,
)
# ── Registries ────────────────────────────────────────────────────────────────
MODEL_NAME = {"fast": "ms_eff_gcvit_b0", "pro": "ms_eff_gcvit_b5"}
EXPLAINER_REGISTRY = {
# low-level branch — local forgery artifacts
"hirescam": HiResCAMExplainer,
"gradcamelementwise": GradCAMElementWiseExplainer,
"layercam": LayerCAMExplainer,
# high-level branch — global semantic structure
"eigengradcam": EigenGradCAMExplainer,
"gradcamplusplus": GradCAMPlusPlusExplainer,
"xgradcam": XGradCAMExplainer,
}
METHODS_BY_BRANCH = {
"low": [
("⭐ LayerCAM", "layercam"),
("HiResCAM", "hirescam"),
("GradCAMElementWise", "gradcamelementwise"),
],
"high": [
("⭐ EigenGradCAM", "eigengradcam"),
("GradCAM++", "gradcamplusplus"),
("XGradCAM", "xgradcam"),
],
}
RECOMMENDED = {"low": "layercam", "high": "eigengradcam"}
CATEGORY_FAKE = 1 # XAI always visualizes forgery evidence
# HF Hub repo naming: ms-eff-gcvit-deepfake-{b0|b5}-{kodf|celeb-df-v2|ff-plus-plus}
_VARIANT_SUFFIX = {"fast": "b0", "pro": "b5"}
_REPO_SUFFIX = {"kodf": "kodf", "celeb_df_v2": "celeb-df-v2", "ff++": "ff-plus-plus"}
def _repo(variant: str, dataset: str) -> str:
return f"KoreaPeter/ms-eff-gcvit-deepfake-{_VARIANT_SUFFIX[variant]}-{_REPO_SUFFIX[dataset]}"
_pipe_cache: dict = {}
_cache: dict = {}
def _get_pipe(variant: str, dataset: str):
"""HF pipeline 로드 — download 카운트 + model card의 Spaces 노출을 트리거."""
key = (variant, dataset)
if key not in _pipe_cache:
device = "cuda" if torch.cuda.is_available() else "cpu"
_pipe_cache[key] = pipeline(
"image-classification",
model=_repo(variant, dataset),
trust_remote_code=True,
device=device,
)
return _pipe_cache[key]
def _get_explainer(variant: str, dataset: str, method: str, branch: str):
key = (variant, dataset, method, branch)
if key not in _cache:
pipe = _get_pipe(variant, dataset)
_cache[key] = EXPLAINER_REGISTRY[method](
model_name=MODEL_NAME[variant],
dataset=dataset,
branch_level=branch,
model=pipe.model.model, # pipeline이 로드한 timm 모델 주입 (가중치 포함)
)
return _cache[key]
# ── Inference ─────────────────────────────────────────────────────────────────
@spaces.GPU
def explain(
image: Image.Image,
variant: str,
dataset: str,
branch: str,
method: str,
display_mode: str,
aug_smooth: bool,
eigen_smooth: bool,
image_weight: float,
threshold: float,
):
if image is None:
raise gr.Error("Please upload an image first.")
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as f:
image.save(f.name)
tmp_path = f.name
try:
explainer = _get_explainer(variant, dataset, method, branch)
# keep cached model on the active (ZeroGPU) device
if torch.cuda.is_available():
explainer.model.to("cuda:0")
explainer.device = "cuda:0"
common = dict(
category=CATEGORY_FAKE,
threshold=threshold,
aug_smooth=aug_smooth,
eigen_smooth=eigen_smooth,
)
if display_mode == "heatmap":
result = explainer.display_heatmap_on_image(tmp_path, image_weight=image_weight, **common)
elif display_mode == "bbox":
result = explainer.display_bbox_on_image(tmp_path, **common)
else: # heatmap_bbox
result = explainer.display_heatmap_bbox_on_image(tmp_path, image_weight=image_weight, **common)
except Exception as e:
msg = str(e)
if "face" in msg.lower() or "NoneType" in msg or "index" in msg.lower():
raise gr.Error("😶 No face detected. Please use an image with a clearly visible face.")
raise gr.Error(f"Explainer error: {e}")
finally:
os.unlink(tmp_path)
return result
# ── Assets (HF CDN absolute URL — keeps GIF animation) ─────────────────────────
_HF_BASE = "https://huggingface.co/spaces/KoreaPeter/DeepFake-Detection-XAI/resolve/main"
DUAL_BRANCH_SRC = f"{_HF_BASE}/dual_branch.gif"
XAI_DEMO_SRC = f"{_HF_BASE}/xai_demo.gif"
# img lives in gr.HTML — gr.Markdown sanitizes <img> tags away
XAI_DEMO_HTML = f"""
<div style="text-align:center; margin:4px 0;">
<img src="{XAI_DEMO_SRC}" style="max-width:95%; border-radius:8px;">
</div>
"""
# ── Hero banner ───────────────────────────────────────────────────────────────
HERO = f"""
<div style="
background: linear-gradient(135deg, #13161f 0%, #3d2f0a 60%, #4a3600 100%);
border-radius: 14px; padding: 36px 40px; margin-bottom: 8px;
border: 1px solid #FFD21E; box-shadow: 0 4px 28px rgba(255,210,30,0.20);
">
<div style="display:flex; align-items:center; gap:16px; margin-bottom:12px;">
<span style="font-size:2.4rem;">🤗</span>
<div>
<h1 style="margin:0; color:#ffffff; font-size:1.9rem; font-weight:700; letter-spacing:-0.5px;">DeepGuard · XAI</h1>
<p style="margin:4px 0 0; color:#FFD21E; font-size:1rem;">Dual-Branch Deepfake Explainability</p>
</div>
</div>
<p style="margin:0 0 16px; color:#e5d5a8; font-size:0.95rem; line-height:1.6;">
Our custom <strong style="color:#ffffff;">MS-EffGCViT</strong> reasons over two parallel streams — a
<strong style="color:#93c5fd;">high-resolution low-level branch</strong> for local forgery artifacts and a
<strong style="color:#fca5a5;">low-resolution high-level branch</strong> for global semantic structure.
This tool mirrors that dual-branch design to reveal <em>where and why</em> a face is flagged as manipulated.
</p>
<div style="text-align:center; margin:8px 0 16px;"><img src="{DUAL_BRANCH_SRC}" style="max-width:70%; border-radius:8px; border:1px solid #FFD21E;"></div>
<div style="display:flex; gap:12px; flex-wrap:wrap;">
<a href="https://github.com/HanMoonSub/DeepGuard" target="_blank"
style="background:#2d2410; color:#FFD21E; padding:7px 16px; border-radius:6px;
text-decoration:none; font-size:0.85rem; border:1px solid #FFD21E;">🔗 GitHub</a>
<a href="https://huggingface.co/KoreaPeter" target="_blank"
style="background:#2d2410; color:#FFD21E; padding:7px 16px; border-radius:6px;
text-decoration:none; font-size:0.85rem; border:1px solid #FFD21E;">🤗 HuggingFace</a>
<a href="https://huggingface.co/spaces/KoreaPeter/DeepFake-Image-Detection" target="_blank"
style="background:#2d2410; color:#FFD21E; padding:7px 16px; border-radius:6px;
text-decoration:none; font-size:0.85rem; border:1px solid #FFD21E;">🕵️ Image Detection</a>
<a href="https://huggingface.co/spaces/KoreaPeter/DeepFake-Video-Detection" target="_blank"
style="background:#2d2410; color:#FFD21E; padding:7px 16px; border-radius:6px;
text-decoration:none; font-size:0.85rem; border:1px solid #FFD21E;">🎬 Video Detection</a>
</div>
</div>
"""
# ── Theme & CSS ───────────────────────────────────────────────────────────────
THEME = gr.themes.Base(
primary_hue="yellow",
neutral_hue="gray",
font=gr.themes.GoogleFont("Inter"),
).set(
body_background_fill="#0b0f19",
body_text_color="#e5e7eb",
block_background_fill="#1a1f2e",
block_border_color="#2d3748",
input_background_fill="#0b0f19",
button_primary_background_fill="#FFD21E",
button_primary_text_color="#1a1a1a",
button_primary_border_color="#FFD21E",
button_primary_background_fill_hover="#FFB700",
button_primary_text_color_hover="#1a1a1a",
)
CSS = """
/* branch/method groups get a colored frame driven by the selected branch */
#branch-group, #method-group {
border: 2px solid #3b82f6 !important; /* default = low (blue) */
border-radius: 10px !important;
padding: 10px 12px !important;
transition: border-color 0.2s ease;
}
#branch-group.branch-high, #method-group.branch-high {
border-color: #ef4444 !important; /* high = red */
}
footer { display:none !important; }
"""
# ── Build UI ──────────────────────────────────────────────────────────────────
with gr.Blocks(title="DeepGuard XAI") as demo:
gr.HTML(HERO)
with gr.Row():
with gr.Column(scale=1, min_width=320):
image_input = gr.Image(type="pil", label="Upload Image", height=400)
with gr.Accordion("🖼️ Example Images — (click to expand)", open=False):
gr.Examples(
examples=["example.JPG"],
inputs=image_input,
label="",
)
gr.Markdown("### ⚡ Model Variant")
variant_radio = gr.Radio(
choices=["fast", "pro"],
value="pro",
label=None,
info="Fast (b0): 8.7M params | Pro (b5): 50.3M params",
)
gr.Markdown("### 🌍 Training Domain")
dataset_radio = gr.Radio(
choices=[
("🇰🇷 KoDF — East Asian faces", "kodf"),
("🌟 Celeb-DF-v2 — Celebrity faces", "celeb_df_v2"),
("🌍 FF++ — Western faces", "ff++"),
],
value="ff++",
label=None,
)
gr.Markdown("### 🧠 Branch Level")
branch_radio = gr.Radio(
choices=[("🔵 Low — local artifacts", "low"), ("🔴 High — global semantics", "high")],
value="high",
label=None,
elem_id="branch-group",
)
gr.Markdown("### 📐 CAM Method")
method_radio = gr.Radio(
choices=METHODS_BY_BRANCH["high"],
value=RECOMMENDED["high"],
label=None,
elem_id="method-group",
)
run_btn = gr.Button("🔬 Visualize", variant="primary", size="lg")
with gr.Column(scale=1, min_width=320):
result_output = gr.Image(label="Forgery Localization", type="numpy", height=400)
gr.Markdown("""
**Branch Guide**
| Branch | Feature Map | Focus | Best For |
|---|---|---|---|
| 🔵 **Low** | High resolution | Local forgery artifacts | Skin texture, boundary blending, compression traces |
| 🔴 **High** | Low resolution | Global semantic structure | Lighting inconsistency, facial geometry, shadow artifacts |
""")
gr.Markdown("### 🎨 Display Guide")
gr.HTML(XAI_DEMO_HTML)
with gr.Accordion("⚡ Model Guide", open=False):
gr.Markdown("""
| | ⚡ Fast (b0) | 🔥 Pro (b5) |
|---|---|---|
| **Params** | 8.7M | 50.3M |
| **Resolution** | 224×224 | 384×384 |
| **Speed** | Faster | Slower |
| **Accuracy** | High | Higher |
""")
with gr.Accordion("🌍 Domain Guide", open=False):
gr.Markdown("""
| Domain | Training Data | Best For |
|---|---|---|
| KoDF | 62K real / 175K fake (Korean) | East Asian faces |
| Celeb-DF-v2 | 890 real / 5.6K fake (Celebrities) | Celebrity faces |
| FF++ | 1K real / 6K fake (4 manipulation types) | Western faces |
""")
with gr.Accordion("📐 CAM Method Guide", open=False):
gr.Markdown("""
| Branch | Method | Core Idea |
|---|---|---|
| 🔵 Low | **HiResCAM** | GradCAM with element-wise activation×gradient; provably faithful |
| 🔵 Low | **GradCAMElementWise** | Element-wise activation×gradient + ReLU before summing |
| 🔵 Low | **⭐ LayerCAM** | Positive-gradient spatial weighting; strong in lower layers |
| 🔴 High | **⭐ EigenGradCAM** | 1st principal component of activations×grad; cleaner GradCAM |
| 🔴 High | **GradCAM++** | GradCAM using second-order gradients |
| 🔴 High | **XGradCAM** | GradCAM with gradients scaled by normalized activations |
""")
with gr.Accordion("🖼️ Display Setting", open=False):
display_radio = gr.Radio(
choices=[
("Heatmap", "heatmap"),
("BBox", "bbox"),
("Heatmap + BBox", "heatmap_bbox"),
],
value="heatmap_bbox",
label="Display Mode",
)
with gr.Row():
image_weight_slider = gr.Slider(
minimum=0.0, maximum=1.0, value=0.7, step=0.05,
label="Image Weight",
info="0.0 heatmap only ←→ 1.0 original only (heatmap modes)",
)
threshold_slider = gr.Slider(
minimum=0.5, maximum=1.0, value=0.9, step=0.05,
label="Threshold",
info="CAM binarization cutoff — 0.9 keeps top-10% regions",
)
with gr.Accordion("🌀 Visualize Smoothing Setting", open=False):
gr.Markdown("Optional smoothing passes over the CAM. Off is recommended for most cases.")
with gr.Row():
aug_smooth_chk = gr.Checkbox(
value=False, label="Aug Smooth",
info="TTA (horizontal flips) before averaging → smoother maps. Slower.",
)
eigen_smooth_chk = gr.Checkbox(
value=False, label="Eigen Smooth",
info="PCA noise reduction → keeps dominant forgery pattern only.",
)
# branch → method choices sync (recommended method becomes default)
def _sync_methods(branch):
choices = METHODS_BY_BRANCH[branch]
return gr.update(choices=choices, value=RECOMMENDED[branch])
branch_radio.change(fn=_sync_methods, inputs=branch_radio, outputs=method_radio)
# branch → recolor the branch/method frames (blue=low, red=high)
branch_radio.change(
fn=None,
inputs=branch_radio,
outputs=None,
js="""(b) => {
const isHigh = (b === 'high');
['branch-group', 'method-group'].forEach(id => {
const el = document.getElementById(id);
if (el) el.classList.toggle('branch-high', isHigh);
});
}""",
)
inputs = [
image_input, variant_radio, dataset_radio, branch_radio, method_radio,
display_radio, aug_smooth_chk, eigen_smooth_chk,
image_weight_slider, threshold_slider,
]
run_btn.click(fn=explain, inputs=inputs, outputs=result_output)
if __name__ == "__main__":
demo.launch(theme=THEME, css=CSS)