File size: 10,754 Bytes
330c69d
9f3f073
330c69d
 
 
 
 
9f3f073
330c69d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f3f073
330c69d
 
 
 
 
 
9f3f073
 
 
 
330c69d
9f3f073
 
 
330c69d
 
 
9f3f073
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330c69d
 
 
 
9f3f073
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330c69d
 
 
9f3f073
 
 
 
330c69d
 
9f3f073
330c69d
9f3f073
 
 
 
 
 
330c69d
9f3f073
 
 
 
 
 
 
 
 
330c69d
9f3f073
330c69d
 
 
9f3f073
 
 
 
 
330c69d
 
9f3f073
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330c69d
9f3f073
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330c69d
 
 
 
9f3f073
330c69d
9f3f073
 
 
330c69d
 
 
 
 
9f3f073
330c69d
9f3f073
 
 
 
 
 
 
 
 
 
 
 
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>'
    )