File size: 10,754 Bytes
0f94be1 0d312b8 0f94be1 0d312b8 0f94be1 0d312b8 0f94be1 0d312b8 0f94be1 0d312b8 0f94be1 0d312b8 0f94be1 0d312b8 0f94be1 0d312b8 0f94be1 0d312b8 0f94be1 0d312b8 0f94be1 0d312b8 0f94be1 0d312b8 0f94be1 0d312b8 0f94be1 0d312b8 0f94be1 0d312b8 0f94be1 0d312b8 0f94be1 0d312b8 0f94be1 0d312b8 0f94be1 0d312b8 | 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 | """Readable presentation of model scores and quantitative attention weights."""
from html import escape
import math
import numpy as np
CARD_CSS = """
.ltf-card{font-family:ui-sans-serif,system-ui,sans-serif;color:#15243c;line-height:1.5}
.ltf-card *{box-sizing:border-box}.ltf-eyebrow{font-size:12px;font-weight:750;letter-spacing:.11em;color:#60748c;text-transform:uppercase}
.ltf-decision{border:1px solid #dce5ef;border-radius:20px;padding:25px;background:#fff;min-height:325px}
.ltf-decision h2{font-size:25px;line-height:1.25;margin:10px 0 20px;letter-spacing:-.03em}
.ltf-score{font-size:58px;line-height:1;font-weight:800;letter-spacing:-.06em;color:var(--accent,#087f85)}
.ltf-score-label{font-size:13px;color:#60748c;margin:8px 0 18px}.ltf-chip{display:inline-block;padding:5px 10px;border-radius:8px;background:#eef3f8;color:#42566e;font-size:12px;font-weight:650;margin-right:5px}
.ltf-mini{display:flex;align-items:center;gap:8px;margin:12px 0;font-size:13px}.ltf-mini b{width:57px}.ltf-mini>span{flex:1;background:#edf1f6;height:7px;border-radius:5px;overflow:hidden}.ltf-mini i{display:block;background:#14888e;height:100%;border-radius:5px}.ltf-mini em{width:45px;text-align:right;font-style:normal;font-variant-numeric:tabular-nums}
.ltf-note{font-size:12px;color:#60748c;margin-top:18px}.ltf-wait{color:#405772;font-size:16px;margin:30px 0 10px}
.ltf-stat-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:2px 0 20px}.ltf-stat{background:#fff;border:1px solid #dce5ef;padding:17px 20px;border-radius:13px}.ltf-stat strong{display:block;font-size:27px;letter-spacing:-.04em}.ltf-stat span{font-size:12px;color:#60748c}
.ltf-chart{background:#fff;border:1px solid #dce5ef;border-radius:14px;padding:18px 16px}.ltf-chart h3{font-size:17px;margin:0 0 5px}.ltf-chart p{font-size:12px;color:#60748c;margin:0 0 12px}.ltf-scale{display:flex;justify-content:space-between;font-size:11px;color:#52687e;margin-top:5px}.ltf-colorbar{height:11px;border-radius:5px;background:linear-gradient(90deg,#23395d,#6886ab,#b4bfcb,#f7a342,#ee503c);margin-top:12px}
.ltf-callout{padding:11px 13px;background:#edf5f8;border-left:3px solid #14888e;border-radius:6px;font-size:12px;color:#33506b;margin-top:12px}
.ltf-hero{font-family:ui-sans-serif,system-ui,sans-serif;padding:29px 32px;border-radius:20px;background:linear-gradient(110deg,#10243e,#153c51);color:white;margin:0 0 24px}.ltf-card .ltf-hero h1{font-size:31px;margin:10px 0;letter-spacing:-.04em;color:#fff!important}.ltf-card .ltf-hero p{font-size:14px;color:#bed2e3!important;margin:7px 0}.ltf-hero .ltf-eyebrow{color:#75dcd2}.ltf-hero .ltf-chip{background:#ffffff12;color:#d9e9f1;border:1px solid #ffffff18;margin-top:12px}
@media(max-width:650px){.ltf-stat-grid{grid-template-columns:repeat(2,1fr)}.ltf-hero{padding:22px}.ltf-hero h1{font-size:25px}.ltf-decision{min-height:250px}.ltf-score{font-size:47px}}
@media(max-width:760px){
.gradio-container{padding:14px 12px 35px!important;min-width:0!important;width:100%!important;max-width:100vw!important}
.gradio-container .main{padding:0!important;min-width:0!important;width:100%!important}
.gradio-container .wrap,.gradio-container .contain{min-width:0!important;max-width:100%!important}
.gradio-container .row,.gradio-container .column,.gradio-container .block{min-width:0!important;max-width:100%!important}
}
"""
def card(content):
return f'<style>{CARD_CSS}</style><div class="ltf-card">{content}</div>'
HEADER = card("""<div class="ltf-hero"><div class="ltf-eyebrow">LTF / MODEL EXPLORER</div>
<h1>Image Detection & Attention Analysis</h1><p>Upload an original image to inspect model scores, tile attention, and patch details.</p>
<span class="ltf-chip">CLIP ViT-L/14</span><span class="ltf-chip">CLIP block 24 features</span>
<span class="ltf-chip">224 × 224 tiles</span><span class="ltf-chip">Research demo</span></div>""")
EMPTY_RESULT = card("""<div class="ltf-decision"><div class="ltf-eyebrow">ANALYSIS RESULT</div>
<h2>Ready to analyze</h2><div class="ltf-wait">Upload an image and select Run Analysis, or click a GenImage example.</div>
<p class="ltf-note">The prediction, generated score, and branch comparison will appear here. Attention analysis opens below.</p></div>""")
def result_card(result):
score = float(result["score_fake"])
generated = result["label"] == 1
accent = "#d75536" if generated else "#087f85"
verdict = "AI-generated image" if generated else "Real image"
branch = {
"fusion": "Fusion",
"level1": "L1 · Patch-level",
"level2": "L2 · View-level",
}[result["branch"]]
bars = ""
for key, label in (("fusion", "Fusion"), ("level1", "L1"), ("level2", "L2")):
value = result["probabilities"][key]["fake"]
bars += (
f'<div class="ltf-mini"><b>{label}</b><span><i style="width:{value*100:.3f}%"></i></span>'
f"<em>{value:.3f}</em></div>"
)
return card(f"""<div class="ltf-decision" style="--accent:{accent};border-top:4px solid {accent}">
<div class="ltf-eyebrow">Model prediction · {branch}</div><h2>{verdict}</h2>
<div class="ltf-score">{score:.3f}</div><div class="ltf-score-label">Generated score · threshold {result['threshold']:.2f}</div>
{bars}<div class="ltf-note">{result['width']:,} × {result['height']:,} px · {result['views']-1} local tiles
<br>Scores are uncalibrated model outputs, not definitive evidence of image origin.</div></div>""")
def stats_card(state):
if not state:
return ""
attention = state["attention"]
layer = next(
(row[2] for row in state["parameters"] if row[1] == "Feature extraction layer"),
"24",
)
stats = [
(str(len(state["boxes"])), "Local tiles + 1 global view"),
(str(attention["l1_heads"].shape[0]), "L1 aggregation heads"),
(
f"{attention['patch'].shape[1]} × {attention['patch'].shape[2]}",
"Patch grid per tile",
),
(str(layer), "CLIP feature block"),
]
return card(
'<div class="ltf-stat-grid">'
+ "".join(
f'<div class="ltf-stat"><strong>{escape(value)}</strong><span>{label}</span></div>'
for value, label in stats
)
+ "</div>"
)
def scale_legend():
return (
'<div class="ltf-colorbar"></div><div class="ltf-scale"><span>≤0.25×</span><span>0.5×</span>'
"<span>1× uniform</span><span>2×</span><span>≥4×</span></div>"
)
def weight_chart(state, selection="mean", level="l1"):
if not state:
return ""
attention = state["attention"]
if level == "l2":
weights = attention["l2"]
labels = ["Global"] + [f"T{i+1}" for i in range(len(weights) - 1)]
title = "L2 View Weights"
else:
weights = (
attention["l1"]
if selection == "mean"
else attention["l1_heads"][int(selection.split("_")[-1]) - 1]
)
labels = [f"T{i+1}" for i in range(len(weights))]
title = "L1 Tile Weights · " + (
"Mean of 8 heads" if selection == "mean" else selection.replace("_", " ")
)
n = len(weights)
order = np.argsort(-weights, kind="stable")[:10]
maximum = max(float(weights.max()), 1 / n) * 1.12
width, left, track, row_height = 430, 63, 290, 32
height = 38 + row_height * len(order)
baseline = left + track * (1 / n) / maximum
svg = [
f'<svg viewBox="0 0 {width} {height}" role="img" aria-label="{escape(title)}" style="width:100%;font-family:system-ui,sans-serif">',
f'<line x1="{baseline:.2f}" y1="8" x2="{baseline:.2f}" y2="{height-22}" stroke="#8394a8" stroke-dasharray="3 4"/>',
]
for rank, index in enumerate(order):
y, value = 12 + rank * row_height, float(weights[index])
fill = (
"#102e4b"
if labels[index] == "Global"
else ("#078b93" if rank == 0 else "#66aab1")
)
svg.extend(
[
f'<text x="0" y="{y+15}" fill="#30465e" font-size="13" font-weight="650">{labels[index]}</text>',
f'<rect x="{left}" y="{y}" width="{track}" height="21" rx="4" fill="#edf2f7"/>',
f'<rect x="{left}" y="{y}" width="{track*value/maximum:.3f}" height="21" rx="4" fill="{fill}">'
f"<title>{labels[index]}: {value:.8f} · {value*n:.4f}× uniform</title></rect>",
f'<text x="{left+track+8}" y="{y+15}" fill="#30465e" font-size="12">{value*100:.2f}%</text>',
]
)
svg.append(
f'<text x="{left}" y="{height-4}" fill="#60748c" font-size="10">Dashed: uniform {100/n:.2f}% · Top 10 shown</text></svg>'
)
ratio = float(weights.max()) * n
message = (
"Attention is nearly uniform, with no pronounced concentration. The heatmap does not exaggerate small differences."
if float(np.ptp(weights)) * n < 0.10
else f"The largest weight is {ratio:.2f}× uniform. Rankings show attention, not evidence of forgery."
)
if level == "l2":
message = (
f"The global view receives {float(weights[0])*100:.2f}%; the overlay shows local tiles only. "
+ message
)
return card(
f'<div class="ltf-chart"><h3>{title}</h3><p>Normalized attention weights sum to 100% across all items.</p>'
+ "".join(svg)
+ scale_legend()
+ f'<div class="ltf-callout">{message}</div></div>'
)
def patch_matrix(state, tile_number):
if not state:
return ""
from attention import attention_levels, colors
index = int(tile_number) - 1
weights = state["attention"]["patch"][index]
palette = colors(attention_levels(weights, weights.size))
h, w = weights.shape
cells = []
for y in range(h):
for x in range(w):
color = "#%02x%02x%02x" % tuple(palette[y, x])
value = float(weights[y, x])
cells.append(
f'<rect x="{x*20}" y="{y*20}" width="20" height="20" fill="{color}" stroke="#ffffff40" stroke-width=".6">'
f"<title>Row {y+1}, column {x+1} · {value*100:.5f}% · {value*weights.size:.3f}× uniform</title></rect>"
)
return card(
f'<div class="ltf-chart"><h3>T{index+1} · Patch Attention Matrix</h3><p>Hover to inspect exact weights. Each cell corresponds to 14 × 14 input pixels.</p>'
f'<svg viewBox="0 0 {w*20} {h*20}" role="img" aria-label="Patch attention matrix" style="width:100%;max-height:420px">'
+ "".join(cells)
+ "</svg>"
+ scale_legend()
+ '<div class="ltf-callout">Shared scale relative to uniform attention: gray is near uniform, blue is lower, and orange-red is higher.</div></div>'
)
|