File size: 6,185 Bytes
646535a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import cv2
import numpy as np
import torch
import torch.nn as nn
from torchvision import models, transforms
from skimage import feature
from scipy.fftpack import dct
from PIL import Image, ImageOps
import streamlit as st

# 引入热力图相关库
from pytorch_grad_cam import GradCAM
from pytorch_grad_cam.utils.model_targets import ClassifierOutputTarget


# ==========================================
# 辅助函数:智能读取图像(修正 EXIF 方向)
# ==========================================
def get_pil_image(image_input):
    if isinstance(image_input, str):
        img = Image.open(image_input)
    else:
        image_input.seek(0)
        img = Image.open(image_input)

    # 强制应用隐藏的 EXIF 旋转信息,还原真实方向
    img = ImageOps.exif_transpose(img)
    return img.convert('RGB')


def get_cv_image(image_input):
    pil_img = get_pil_image(image_input)
    # 保证 OpenCV 提取物理特征时,方向与 PIL 绝对一致
    return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)


# ==========================================
# 第一部分:传统物理特征检测 (权重 15%)
# ==========================================
def extract_traditional_features(image_input):
    img = get_cv_image(image_input)
    if img is None:
        return 0.0

    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # 1. LBP 局部二值模式
    lbp = feature.local_binary_pattern(gray, P=8, R=1, method="uniform")
    hist, _ = np.histogram(lbp.ravel(), bins=np.arange(0, 11), density=True)
    lbp_entropy = -np.sum(hist * np.log2(hist + 1e-7))

    # 2. DCT 离散余弦变换
    dct_data = dct(dct(gray.T, norm='ortho').T, norm='ortho')
    high_freq = np.sum(np.abs(dct_data[10:, 10:]))
    total_energy = np.sum(np.abs(dct_data))
    dct_ratio = high_freq / (total_energy + 1e-7)

    # 3. FFT 快速傅里叶变换
    f = np.fft.fft2(gray)
    fshift = np.fft.fftshift(f)
    magnitude = 20 * np.log(np.abs(fshift) + 1)
    h, w = magnitude.shape
    center_h, center_w = h // 2, w // 2
    top_left = magnitude[0:center_h, 0:center_w]
    bottom_right = np.flip(magnitude[center_h + (h % 2):, center_w + (w % 2):])

    if top_left.shape != bottom_right.shape:
        bottom_right = cv2.resize(bottom_right, (top_left.shape[1], top_left.shape[0]))

    fft_sym_error = np.mean(np.abs(top_left - bottom_right))

    score = 0.0
    if lbp_entropy > 3.6: score += 0.3
    if dct_ratio < 0.985: score += 0.1
    if fft_sym_error > 13.8: score += 0.1

    return min(score, 1.0)


# ==========================================
# 第二部分:深度模型缓存与提取
# ==========================================
@st.cache_resource
def load_deep_image_model():
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model = models.mobilenet_v2(weights=None)
    num_ftrs = model.classifier[1].in_features
    model.classifier[1] = nn.Linear(num_ftrs, 2)

    # 加载自己炼丹生成的权重文件
    model.load_state_dict(torch.load('mobilenet_finetuned.pth', map_location=device))
    model = model.to(device)
    model.eval()

    return model, device


def extract_deep_features(image_input, model, device):
    pil_img = get_pil_image(image_input)
    transform = transforms.Compose([
        transforms.Resize((224, 224)),
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
    ])
    input_tensor = transform(pil_img).unsqueeze(0).to(device)

    with torch.no_grad():
        outputs = model(input_tensor)
        probs = torch.softmax(outputs, dim=1)
        fake_prob = probs[0][0].item()

    return fake_prob


# ==========================================
# 第三部分:多模态加权融合引擎
# ==========================================
def analyze_image(image_input):
    trad_score = extract_traditional_features(image_input)
    model, device = load_deep_image_model()
    deep_score = extract_deep_features(image_input, model, device)

    final_prob = (trad_score * 0.15) + (deep_score * 0.85)

    return {
        'traditional_score': trad_score,
        'deep_score': deep_score,
        'final_probability': final_prob
    }


# ==========================================
# 第四部分:XAI 热力图渲染引擎
# ==========================================
def generate_image_heatmap(image_input, model, device):
    """使用 Grad-CAM 生成热力图,并无损拉伸回原图尺寸,带动态透明度"""
    # ⚠️ 获取原图,绝对没有任何 resize 操作干扰!
    original_pil_img = get_pil_image(image_input)
    orig_width, orig_height = original_pil_img.size

    transform = transforms.Compose([
        transforms.Resize((224, 224)),
        transforms.ToTensor(),
        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
    ])
    input_tensor = transform(original_pil_img).unsqueeze(0).to(device)

    target_layers = [model.features[-1]]

    try:
        with GradCAM(model=model, target_layers=target_layers) as cam:
            targets = [ClassifierOutputTarget(0)]
            # 跑出 224x224 的 0~1 原始热力图矩阵
            grayscale_cam_224 = cam(input_tensor=input_tensor, targets=targets)[0, :]

        # 将 224x224 的热力图拉伸回真实的高清原图宽高!
        grayscale_cam_resized = cv2.resize(grayscale_cam_224, (orig_width, orig_height))

        heatmap_color = cv2.applyColorMap(np.uint8(255 * grayscale_cam_resized), cv2.COLORMAP_JET)
        heatmap_color = cv2.cvtColor(heatmap_color, cv2.COLOR_BGR2RGB)
        heatmap_color = np.float32(heatmap_color) / 255.0

        orig_img_array = np.array(original_pil_img, dtype=np.float32) / 255.0

        alpha = grayscale_cam_resized[..., np.newaxis]
        alpha = np.power(alpha, 1.5) * 0.65

        visualization = orig_img_array * (1 - alpha) + heatmap_color * alpha
        return np.uint8(255 * visualization)

    except Exception as e:
        return np.array(original_pil_img)