Spaces:
Running on Zero
Running on Zero
Update app.py
Browse files
app.py
CHANGED
|
@@ -6,15 +6,10 @@ import os
|
|
| 6 |
from PIL import Image
|
| 7 |
|
| 8 |
import deepguard # noqa: F401 # registers ms_eff_gcvit_* into timm
|
| 9 |
-
import numpy as np
|
| 10 |
from explainability import (
|
| 11 |
HiResCAMExplainer, GradCAMElementWiseExplainer, LayerCAMExplainer,
|
| 12 |
EigenGradCAMExplainer, GradCAMPlusPlusExplainer, XGradCAMExplainer,
|
| 13 |
)
|
| 14 |
-
from explainability.metrics.road import (
|
| 15 |
-
ROADMostRelevantFirstAverage, ROADLeastRelevantFirstAverage,
|
| 16 |
-
)
|
| 17 |
-
from explainability.utils.model_targets import BinaryClassifierOutputSigmoidTarget
|
| 18 |
|
| 19 |
# ── Registries ────────────────────────────────────────────────────────────────
|
| 20 |
MODEL_NAME = {"fast": "ms_eff_gcvit_b0", "pro": "ms_eff_gcvit_b5"}
|
|
@@ -61,62 +56,6 @@ def _get_explainer(variant: str, dataset: str, method: str, branch: str):
|
|
| 61 |
return _cache[key]
|
| 62 |
|
| 63 |
|
| 64 |
-
# ── ROAD metric table (video-style dark table) ────────────────────────────────
|
| 65 |
-
def _metric_bar(val: float, color: str) -> str:
|
| 66 |
-
w = int(max(0.0, min(1.0, val)) * 100)
|
| 67 |
-
return (
|
| 68 |
-
'<div style="background:#0b0f19;border-radius:4px;height:14px;'
|
| 69 |
-
'width:160px;overflow:hidden;">'
|
| 70 |
-
f'<div style="background:{color};width:{w}%;height:100%;border-radius:4px;"></div></div>'
|
| 71 |
-
)
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
def _build_metric_table(morf: float, lerf: float) -> str:
|
| 75 |
-
rows = [
|
| 76 |
-
("MORF", "Most Relevant First", morf, "#f87171", "↓ lower = more faithful"),
|
| 77 |
-
("LERF", "Least Relevant First", lerf, "#4ade80", "↑ higher = more faithful"),
|
| 78 |
-
]
|
| 79 |
-
body = ""
|
| 80 |
-
for short, full, val, color, interp in rows:
|
| 81 |
-
body += f"""
|
| 82 |
-
<tr style="background:#12161f; border-bottom:1px solid #2d3748;">
|
| 83 |
-
<td style="padding:10px 14px; color:#FFD21E; font-weight:700;">{short}</td>
|
| 84 |
-
<td style="padding:10px 14px; color:#9ca3af;">{full}</td>
|
| 85 |
-
<td style="padding:10px 14px;">{_metric_bar(val, color)}</td>
|
| 86 |
-
<td style="padding:10px 14px; color:{color}; font-family:monospace; font-weight:700;">{val:.4f}</td>
|
| 87 |
-
<td style="padding:10px 14px; color:#9ca3af; font-size:13px;">{interp}</td>
|
| 88 |
-
</tr>"""
|
| 89 |
-
|
| 90 |
-
return f"""
|
| 91 |
-
<div style="overflow-x:auto; margin-top:8px;">
|
| 92 |
-
<table style="width:100%; border-collapse:collapse; font-size:14px;
|
| 93 |
-
background:#0b0f19; border-radius:8px; overflow:hidden;">
|
| 94 |
-
<thead>
|
| 95 |
-
<tr style="background:#FFD21E; color:#1a1a1a;">
|
| 96 |
-
<th style="padding:10px 14px; text-align:left;">Metric</th>
|
| 97 |
-
<th style="padding:10px 14px; text-align:left;">Perturbation</th>
|
| 98 |
-
<th style="padding:10px 14px; text-align:left;">Faithfulness</th>
|
| 99 |
-
<th style="padding:10px 14px; text-align:left;">Score</th>
|
| 100 |
-
<th style="padding:10px 14px; text-align:left;">Interpretation</th>
|
| 101 |
-
</tr>
|
| 102 |
-
</thead>
|
| 103 |
-
<tbody>{body}</tbody>
|
| 104 |
-
</table>
|
| 105 |
-
</div>"""
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
def _evaluate_road(explainer, img_path: str) -> str:
|
| 109 |
-
torch.manual_seed(2026)
|
| 110 |
-
np.random.seed(2026)
|
| 111 |
-
pct = [20, 40, 60, 80] # reduced from 9 → 4 for speed
|
| 112 |
-
grayscale_cam, tensor, _ = explainer._build_grayscale_cam(img_path)
|
| 113 |
-
cams = grayscale_cam[np.newaxis, ...] # (1, H, W)
|
| 114 |
-
targets = [BinaryClassifierOutputSigmoidTarget()]
|
| 115 |
-
morf = float(ROADMostRelevantFirstAverage(pct)(tensor, cams, targets, explainer.model)[0])
|
| 116 |
-
lerf = float(ROADLeastRelevantFirstAverage(pct)(tensor, cams, targets, explainer.model)[0])
|
| 117 |
-
return _build_metric_table(morf, lerf)
|
| 118 |
-
|
| 119 |
-
|
| 120 |
# ── Inference ─────────────────────────────────────────────────────────────────
|
| 121 |
@spaces.GPU
|
| 122 |
def explain(
|
|
@@ -170,31 +109,6 @@ def explain(
|
|
| 170 |
return result
|
| 171 |
|
| 172 |
|
| 173 |
-
# ── ROAD evaluation (separate button) ─────────────────────────────────────────
|
| 174 |
-
@spaces.GPU
|
| 175 |
-
def evaluate_metrics(image, variant, dataset, branch, method):
|
| 176 |
-
if image is None:
|
| 177 |
-
raise gr.Error("Please upload an image first.")
|
| 178 |
-
|
| 179 |
-
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as f:
|
| 180 |
-
image.save(f.name)
|
| 181 |
-
tmp_path = f.name
|
| 182 |
-
|
| 183 |
-
try:
|
| 184 |
-
explainer = _get_explainer(variant, dataset, method, branch)
|
| 185 |
-
if torch.cuda.is_available():
|
| 186 |
-
explainer.model.to("cuda:0")
|
| 187 |
-
explainer.device = "cuda:0"
|
| 188 |
-
return _evaluate_road(explainer, tmp_path)
|
| 189 |
-
except Exception as e:
|
| 190 |
-
msg = str(e)
|
| 191 |
-
if "face" in msg.lower() or "NoneType" in msg or "index" in msg.lower():
|
| 192 |
-
raise gr.Error("😶 No face detected. Please use an image with a clearly visible face.")
|
| 193 |
-
raise gr.Error(f"ROAD evaluation error: {e}")
|
| 194 |
-
finally:
|
| 195 |
-
os.unlink(tmp_path)
|
| 196 |
-
|
| 197 |
-
|
| 198 |
# ── Assets (HF CDN absolute URL — keeps GIF animation) ─────────────────────────
|
| 199 |
_HF_BASE = "https://huggingface.co/spaces/KoreaPeter/DeepFake-Detection-XAI/resolve/main"
|
| 200 |
DUAL_BRANCH_SRC = f"{_HF_BASE}/dual_branch.gif"
|
|
@@ -410,15 +324,6 @@ with gr.Blocks(title="DeepGuard XAI") as demo:
|
|
| 410 |
info="PCA noise reduction → keeps dominant forgery pattern only.",
|
| 411 |
)
|
| 412 |
|
| 413 |
-
with gr.Accordion("📊 Explainability Metrics — ROAD", open=True):
|
| 414 |
-
gr.Markdown(
|
| 415 |
-
"CAM **faithfulness** via ROAD (perturbation-based). "
|
| 416 |
-
"Pixels are removed by relevance and the drop in prediction confidence is measured. "
|
| 417 |
-
"⚠️ Heavier than visualization — run only when needed."
|
| 418 |
-
)
|
| 419 |
-
eval_btn = gr.Button("📊 Evaluate ROAD", variant="secondary")
|
| 420 |
-
metric_output = gr.HTML("")
|
| 421 |
-
|
| 422 |
# branch → method choices sync (recommended method becomes default)
|
| 423 |
def _sync_methods(branch):
|
| 424 |
choices = METHODS_BY_BRANCH[branch]
|
|
@@ -446,11 +351,6 @@ with gr.Blocks(title="DeepGuard XAI") as demo:
|
|
| 446 |
image_weight_slider, threshold_slider,
|
| 447 |
]
|
| 448 |
run_btn.click(fn=explain, inputs=inputs, outputs=result_output)
|
| 449 |
-
eval_btn.click(
|
| 450 |
-
fn=evaluate_metrics,
|
| 451 |
-
inputs=[image_input, variant_radio, dataset_radio, branch_radio, method_radio],
|
| 452 |
-
outputs=metric_output,
|
| 453 |
-
)
|
| 454 |
|
| 455 |
if __name__ == "__main__":
|
| 456 |
demo.launch(theme=THEME, css=CSS)
|
|
|
|
| 6 |
from PIL import Image
|
| 7 |
|
| 8 |
import deepguard # noqa: F401 # registers ms_eff_gcvit_* into timm
|
|
|
|
| 9 |
from explainability import (
|
| 10 |
HiResCAMExplainer, GradCAMElementWiseExplainer, LayerCAMExplainer,
|
| 11 |
EigenGradCAMExplainer, GradCAMPlusPlusExplainer, XGradCAMExplainer,
|
| 12 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
# ── Registries ────────────────────────────────────────────────────────────────
|
| 15 |
MODEL_NAME = {"fast": "ms_eff_gcvit_b0", "pro": "ms_eff_gcvit_b5"}
|
|
|
|
| 56 |
return _cache[key]
|
| 57 |
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
# ── Inference ─────────────────────────────────────────────────────────────────
|
| 60 |
@spaces.GPU
|
| 61 |
def explain(
|
|
|
|
| 109 |
return result
|
| 110 |
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
# ── Assets (HF CDN absolute URL — keeps GIF animation) ─────────────────────────
|
| 113 |
_HF_BASE = "https://huggingface.co/spaces/KoreaPeter/DeepFake-Detection-XAI/resolve/main"
|
| 114 |
DUAL_BRANCH_SRC = f"{_HF_BASE}/dual_branch.gif"
|
|
|
|
| 324 |
info="PCA noise reduction → keeps dominant forgery pattern only.",
|
| 325 |
)
|
| 326 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
# branch → method choices sync (recommended method becomes default)
|
| 328 |
def _sync_methods(branch):
|
| 329 |
choices = METHODS_BY_BRANCH[branch]
|
|
|
|
| 351 |
image_weight_slider, threshold_slider,
|
| 352 |
]
|
| 353 |
run_btn.click(fn=explain, inputs=inputs, outputs=result_output)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
|
| 355 |
if __name__ == "__main__":
|
| 356 |
demo.launch(theme=THEME, css=CSS)
|