Spaces:
Runtime error
Runtime error
Commit Β·
457cfc5
1
Parent(s): 2cfb76f
Deploy deepfake detector backend
Browse files- Dockerfile +13 -0
- app.py +229 -0
- best_model.pth +3 -0
- check_weights.py +39 -0
- download_model.py +16 -0
- requirements.txt +9 -0
- scaler.pkl +3 -0
- svm_linear_model.onnx +3 -0
- testbatch.py +116 -0
Dockerfile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY . .
|
| 6 |
+
|
| 7 |
+
RUN pip install --no-cache-dir flask flask-cors onnxruntime numpy Pillow scipy torch timm opencv-python-headless scikit-learn joblib
|
| 8 |
+
|
| 9 |
+
ENV PORT=7860
|
| 10 |
+
|
| 11 |
+
EXPOSE 7860
|
| 12 |
+
|
| 13 |
+
CMD ["python", "app.py"]
|
app.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Deepfake detector backend β with tuned threshold.
|
| 3 |
+
|
| 4 |
+
Pipeline:
|
| 5 |
+
image (BGR)
|
| 6 |
+
βββ 4-channel tensor (RGB normalized + FFT power spectrum) β EfficientNet-B0 β 1280
|
| 7 |
+
βββ FFT azimuthal avg at 256Γ256 β 128
|
| 8 |
+
βββ noise (gray - GaussianBlur) FFT azimuthal avg at 256Γ256 β 128
|
| 9 |
+
ββββ
|
| 10 |
+
1536 β StandardScaler β SVM (ONNX)
|
| 11 |
+
|
| 12 |
+
IMPORTANT: The SVM was trained on StandardScaler-normalized features.
|
| 13 |
+
scaler.pkl must be present in the backend folder.
|
| 14 |
+
Without it, predictions collapse to 0%/100%.
|
| 15 |
+
|
| 16 |
+
Threshold tuned to 0.35 based on independent test set analysis.
|
| 17 |
+
|
| 18 |
+
Run:
|
| 19 |
+
pip install flask flask-cors onnxruntime torch timm opencv-python-headless scipy numpy pillow scikit-learn
|
| 20 |
+
python app.py
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
from flask import Flask, request, jsonify
|
| 24 |
+
from flask_cors import CORS
|
| 25 |
+
import onnxruntime as ort
|
| 26 |
+
import numpy as np
|
| 27 |
+
import cv2
|
| 28 |
+
import io
|
| 29 |
+
import torch
|
| 30 |
+
import torch.nn as nn
|
| 31 |
+
import timm
|
| 32 |
+
from torchvision import transforms
|
| 33 |
+
from scipy import ndimage
|
| 34 |
+
from PIL import Image
|
| 35 |
+
import joblib
|
| 36 |
+
import os
|
| 37 |
+
|
| 38 |
+
app = Flask(__name__)
|
| 39 |
+
CORS(app)
|
| 40 |
+
|
| 41 |
+
# ---------------------------------------------------------------------------
|
| 42 |
+
# Config
|
| 43 |
+
# ---------------------------------------------------------------------------
|
| 44 |
+
FFT_SIZE = 256
|
| 45 |
+
AZ_BINS = FFT_SIZE // 2 # 128
|
| 46 |
+
|
| 47 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 48 |
+
CNN_WEIGHTS_PATH = os.path.join(BASE_DIR, "best_model.pth")
|
| 49 |
+
SVM_ONNX_PATH = os.path.join(BASE_DIR, "svm_linear_model.onnx")
|
| 50 |
+
SCALER_PATH = os.path.join(BASE_DIR, "scaler.pkl")
|
| 51 |
+
|
| 52 |
+
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 53 |
+
|
| 54 |
+
DECISION_THRESHOLD = 0.35
|
| 55 |
+
CALIBRATION_TEMPERATURE = 3.0
|
| 56 |
+
|
| 57 |
+
# ---------------------------------------------------------------------------
|
| 58 |
+
# CNN feature extractor (4-channel EfficientNet-B0)
|
| 59 |
+
# ---------------------------------------------------------------------------
|
| 60 |
+
class DeepfakeDetector(nn.Module):
|
| 61 |
+
def __init__(self):
|
| 62 |
+
super().__init__()
|
| 63 |
+
self.backbone = timm.create_model("efficientnet_b0", pretrained=False, num_classes=0)
|
| 64 |
+
old_conv = self.backbone.conv_stem
|
| 65 |
+
new_conv = nn.Conv2d(
|
| 66 |
+
4, old_conv.out_channels,
|
| 67 |
+
kernel_size=old_conv.kernel_size,
|
| 68 |
+
stride=old_conv.stride,
|
| 69 |
+
padding=old_conv.padding,
|
| 70 |
+
bias=old_conv.bias is not None,
|
| 71 |
+
)
|
| 72 |
+
self.backbone.conv_stem = new_conv
|
| 73 |
+
self.head = nn.Sequential(
|
| 74 |
+
nn.Dropout(0.3), nn.Linear(1280, 256), nn.ReLU(),
|
| 75 |
+
nn.Dropout(0.2), nn.Linear(256, 1),
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
def forward(self, x):
|
| 79 |
+
return self.backbone(x)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
feature_model = DeepfakeDetector().to(DEVICE)
|
| 83 |
+
state = torch.load(CNN_WEIGHTS_PATH, map_location=DEVICE, weights_only=False)
|
| 84 |
+
feature_model.load_state_dict(state, strict=False)
|
| 85 |
+
feature_model.eval()
|
| 86 |
+
print("β
CNN feature extractor loaded")
|
| 87 |
+
|
| 88 |
+
svm_session = ort.InferenceSession(SVM_ONNX_PATH, providers=["CPUExecutionProvider"])
|
| 89 |
+
SVM_INPUT_NAME = svm_session.get_inputs()[0].name
|
| 90 |
+
print(f"β
SVM ONNX loaded β input '{SVM_INPUT_NAME}', expects {svm_session.get_inputs()[0].shape}")
|
| 91 |
+
print(f"β
Decision threshold: {DECISION_THRESHOLD} (lowered from default 0.5)")
|
| 92 |
+
print("\n--- SVM ONNX Outputs ---")
|
| 93 |
+
for out in svm_session.get_outputs():
|
| 94 |
+
print(f" name={out.name!r} shape={out.shape} type={out.type}")
|
| 95 |
+
print("------------------------\n")
|
| 96 |
+
|
| 97 |
+
if os.path.exists(SCALER_PATH):
|
| 98 |
+
feature_scaler = joblib.load(SCALER_PATH)
|
| 99 |
+
print(f"β
StandardScaler loaded from {SCALER_PATH}")
|
| 100 |
+
print(f" mean range: [{feature_scaler.mean_.min():.3f}, {feature_scaler.mean_.max():.3f}]")
|
| 101 |
+
print(f" scale range: [{feature_scaler.scale_.min():.3f}, {feature_scaler.scale_.max():.3f}]")
|
| 102 |
+
else:
|
| 103 |
+
feature_scaler = None
|
| 104 |
+
print(f"β οΈ WARNING: {SCALER_PATH} not found β predictions will be 0% or 100%!")
|
| 105 |
+
|
| 106 |
+
torch_transform = transforms.Compose([
|
| 107 |
+
transforms.ToPILImage(),
|
| 108 |
+
transforms.Resize((224, 224)),
|
| 109 |
+
transforms.ToTensor(),
|
| 110 |
+
transforms.Normalize(mean=[0.485, 0.456, 0.406],
|
| 111 |
+
std=[0.229, 0.224, 0.225]),
|
| 112 |
+
])
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def compute_azimuthal_average(spectrum_2d: np.ndarray) -> np.ndarray:
|
| 116 |
+
h, w = spectrum_2d.shape
|
| 117 |
+
cy, cx = h // 2, w // 2
|
| 118 |
+
Y, X = np.ogrid[:h, :w]
|
| 119 |
+
r = np.sqrt((X - cx) ** 2 + (Y - cy) ** 2).astype(int)
|
| 120 |
+
max_r = min(cy, cx)
|
| 121 |
+
return ndimage.mean(spectrum_2d, labels=r, index=np.arange(0, max_r))
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def extract_combined_features(img_bgr: np.ndarray) -> np.ndarray:
|
| 125 |
+
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
|
| 126 |
+
tensor_3ch = torch_transform(img_rgb)
|
| 127 |
+
|
| 128 |
+
gray224 = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
|
| 129 |
+
gray224 = cv2.resize(gray224, (224, 224)).astype(np.float32)
|
| 130 |
+
f = np.fft.fft2(gray224)
|
| 131 |
+
f_shift = np.fft.fftshift(f)
|
| 132 |
+
ps = np.log1p(np.abs(f_shift) ** 2)
|
| 133 |
+
ps_norm = (ps - ps.min()) / (ps.max() - ps.min() + 1e-8)
|
| 134 |
+
fft_ch = torch.tensor(ps_norm, dtype=torch.float32).unsqueeze(0)
|
| 135 |
+
tensor_4ch = torch.cat([tensor_3ch, fft_ch], dim=0).unsqueeze(0).to(DEVICE)
|
| 136 |
+
|
| 137 |
+
with torch.no_grad():
|
| 138 |
+
cnn_feat = feature_model(tensor_4ch).cpu().numpy().flatten()
|
| 139 |
+
|
| 140 |
+
gray = cv2.resize(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY),
|
| 141 |
+
(FFT_SIZE, FFT_SIZE)).astype(np.float32)
|
| 142 |
+
|
| 143 |
+
f2 = np.fft.fft2(gray)
|
| 144 |
+
f2_shift = np.fft.fftshift(f2)
|
| 145 |
+
power = np.log1p(np.abs(f2_shift) ** 2)
|
| 146 |
+
az_avg = compute_azimuthal_average(power)
|
| 147 |
+
|
| 148 |
+
blurred = cv2.GaussianBlur(gray, (5, 5), 1.0)
|
| 149 |
+
noise = gray - blurred
|
| 150 |
+
nf = np.fft.fft2(noise)
|
| 151 |
+
nf_shift = np.fft.fftshift(nf)
|
| 152 |
+
noise_power = np.log1p(np.abs(nf_shift) ** 2)
|
| 153 |
+
noise_az = compute_azimuthal_average(noise_power)
|
| 154 |
+
|
| 155 |
+
combined = np.concatenate([cnn_feat, az_avg, noise_az]).astype(np.float32)
|
| 156 |
+
assert combined.shape == (1536,), f"Bad feature dim: {combined.shape}"
|
| 157 |
+
return combined
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def get_confidence_level(p_fake: float) -> str:
|
| 161 |
+
distance = abs(p_fake - DECISION_THRESHOLD)
|
| 162 |
+
if distance < 0.10:
|
| 163 |
+
return "uncertain"
|
| 164 |
+
elif distance < 0.25:
|
| 165 |
+
return "low"
|
| 166 |
+
elif distance < 0.40:
|
| 167 |
+
return "medium"
|
| 168 |
+
else:
|
| 169 |
+
return "high"
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
@app.route("/", methods=["GET"])
|
| 173 |
+
def health():
|
| 174 |
+
return jsonify({"status": "ok", "message": "Deepfake detector backend is running."})
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
@app.route("/analyze", methods=["POST"])
|
| 178 |
+
def analyze():
|
| 179 |
+
if "image" not in request.files:
|
| 180 |
+
return jsonify({"error": "No image"}), 400
|
| 181 |
+
|
| 182 |
+
try:
|
| 183 |
+
raw = request.files["image"].read()
|
| 184 |
+
pil = Image.open(io.BytesIO(raw)).convert("RGB")
|
| 185 |
+
img_rgb = np.array(pil)
|
| 186 |
+
img_bgr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR)
|
| 187 |
+
|
| 188 |
+
features = extract_combined_features(img_bgr).reshape(1, -1)
|
| 189 |
+
|
| 190 |
+
if feature_scaler is not None:
|
| 191 |
+
features = feature_scaler.transform(features).astype(np.float32)
|
| 192 |
+
|
| 193 |
+
label_arr, prob_list = svm_session.run(None, {SVM_INPUT_NAME: features})
|
| 194 |
+
for out in svm_session.get_outputs():
|
| 195 |
+
print(out.name, out.shape, out.type)
|
| 196 |
+
prob_dict = prob_list[0]
|
| 197 |
+
p_fake_raw = float(prob_dict[1])
|
| 198 |
+
|
| 199 |
+
eps = 1e-6
|
| 200 |
+
p_clipped = float(np.clip(p_fake_raw, eps, 1.0 - eps))
|
| 201 |
+
logit_p = np.log(p_clipped / (1.0 - p_clipped))
|
| 202 |
+
p_fake = float(1.0 / (1.0 + np.exp(-logit_p / CALIBRATION_TEMPERATURE)))
|
| 203 |
+
p_real = 1.0 - p_fake
|
| 204 |
+
|
| 205 |
+
is_fake = bool(int(label_arr[0]) == 1)
|
| 206 |
+
confidence = get_confidence_level(p_fake)
|
| 207 |
+
|
| 208 |
+
display_percent = round(p_fake * 100, 2)
|
| 209 |
+
print(f"DEBUG: raw={p_fake_raw:.4f} temp_scaled={p_fake:.4f} "
|
| 210 |
+
f"label={'FAKE' if is_fake else 'REAL'} confidence={confidence}")
|
| 211 |
+
|
| 212 |
+
return jsonify({
|
| 213 |
+
"probability": display_percent,
|
| 214 |
+
"label": "AI Generated / Fake" if is_fake else "Authentic Media",
|
| 215 |
+
"is_fake": is_fake,
|
| 216 |
+
"confidence": confidence,
|
| 217 |
+
"p_real": round(p_real * 100, 2),
|
| 218 |
+
"p_fake": round(p_fake * 100, 2),
|
| 219 |
+
"threshold_used": DECISION_THRESHOLD,
|
| 220 |
+
})
|
| 221 |
+
|
| 222 |
+
except Exception as e:
|
| 223 |
+
import traceback; traceback.print_exc()
|
| 224 |
+
return jsonify({"error": f"Failed: {e}"}), 500
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
if __name__ == "__main__":
|
| 228 |
+
port = int(os.environ.get("PORT", 5000))
|
| 229 |
+
app.run(host="0.0.0.0", port=port, debug=False)
|
best_model.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5f4f2b8f38d45ae4db43f571308f1c005673dd47efb5d3725c6ae1485836f30a
|
| 3 |
+
size 17647947
|
check_weights.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
import timm
|
| 4 |
+
|
| 5 |
+
class DeepfakeDetector(nn.Module):
|
| 6 |
+
def __init__(self):
|
| 7 |
+
super().__init__()
|
| 8 |
+
self.backbone = timm.create_model("efficientnet_b0", pretrained=False, num_classes=0)
|
| 9 |
+
old_conv = self.backbone.conv_stem
|
| 10 |
+
new_conv = nn.Conv2d(4, old_conv.out_channels,
|
| 11 |
+
kernel_size=old_conv.kernel_size, stride=old_conv.stride,
|
| 12 |
+
padding=old_conv.padding, bias=old_conv.bias is not None)
|
| 13 |
+
self.backbone.conv_stem = new_conv
|
| 14 |
+
self.head = nn.Sequential(
|
| 15 |
+
nn.Dropout(0.3), nn.Linear(1280, 256), nn.ReLU(),
|
| 16 |
+
nn.Dropout(0.2), nn.Linear(256, 1))
|
| 17 |
+
def forward(self, x):
|
| 18 |
+
return self.backbone(x)
|
| 19 |
+
|
| 20 |
+
model = DeepfakeDetector()
|
| 21 |
+
state = torch.load("best_model.pth", map_location="cpu", weights_only=False)
|
| 22 |
+
|
| 23 |
+
print(f"Keys in checkpoint: {len(state)}")
|
| 24 |
+
print(f"Keys in model: {len(model.state_dict())}")
|
| 25 |
+
|
| 26 |
+
result = model.load_state_dict(state, strict=False)
|
| 27 |
+
print(f"\nMissing keys (not loaded, stay random!): {len(result.missing_keys)}")
|
| 28 |
+
for k in result.missing_keys[:20]:
|
| 29 |
+
print(f" {k}")
|
| 30 |
+
|
| 31 |
+
print(f"\nUnexpected keys (in file but unused): {len(result.unexpected_keys)}")
|
| 32 |
+
for k in result.unexpected_keys[:20]:
|
| 33 |
+
print(f" {k}")
|
| 34 |
+
|
| 35 |
+
# Check sample tensor for sanity
|
| 36 |
+
ckpt_first = list(state.keys())[0]
|
| 37 |
+
model_first = list(model.state_dict().keys())[0]
|
| 38 |
+
print(f"\nCheckpoint first key: {ckpt_first} shape={state[ckpt_first].shape}")
|
| 39 |
+
print(f"Model first key: {model_first} shape={model.state_dict()[model_first].shape}")
|
download_model.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import os
|
| 3 |
+
|
| 4 |
+
# Link to the specific v2 ONNX model
|
| 5 |
+
url = "https://huggingface.co/onnx-community/Deep-Fake-Detector-v2-Model-ONNX/resolve/main/onnx/model.onnx"
|
| 6 |
+
|
| 7 |
+
def download_model():
|
| 8 |
+
print("Downloading Deep-Fake-Detector-v2 (ONNX)... This may take a moment.")
|
| 9 |
+
response = requests.get(url, stream=True)
|
| 10 |
+
with open("model.onnx", "wb") as f:
|
| 11 |
+
for chunk in response.iter_content(chunk_size=8192):
|
| 12 |
+
f.write(chunk)
|
| 13 |
+
print("Download Complete! 'model.onnx' is ready.")
|
| 14 |
+
|
| 15 |
+
if __name__ == "__main__":
|
| 16 |
+
download_model()
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask
|
| 2 |
+
flask-cors
|
| 3 |
+
onnxruntime
|
| 4 |
+
numpy
|
| 5 |
+
Pillow
|
| 6 |
+
scipy
|
| 7 |
+
torch
|
| 8 |
+
timm
|
| 9 |
+
opencv-python-headless
|
scaler.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d033f1eadf524df6115c85e801ca028efd6786849750bbb43d5fdc01251b412d
|
| 3 |
+
size 37479
|
svm_linear_model.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3903d7ded59104307e8c419bfb11c00914ee64a5e1609c80d3659bf45a4b32a3
|
| 3 |
+
size 1189973
|
testbatch.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Batch-test backend menggunakan satu folder dengan naming convention:
|
| 3 |
+
r1, r2, r3, ... β real
|
| 4 |
+
f1, f2, f3, ... β fake
|
| 5 |
+
|
| 6 |
+
Usage:
|
| 7 |
+
1. Pastiin app.py jalan di terminal lain
|
| 8 |
+
2. python test_batch.py "C:/Users/yourname/Downloads"
|
| 9 |
+
atau
|
| 10 |
+
python test_batch.py ~/Downloads
|
| 11 |
+
"""
|
| 12 |
+
import os, sys, glob, re, requests
|
| 13 |
+
from collections import Counter
|
| 14 |
+
|
| 15 |
+
if len(sys.argv) < 2:
|
| 16 |
+
print("Usage: python test_batch.py /path/to/folder")
|
| 17 |
+
print('Windows example: python test_batch.py "C:\\Users\\YourName\\Downloads"')
|
| 18 |
+
print("Mac example: python test_batch.py ~/Downloads")
|
| 19 |
+
sys.exit(1)
|
| 20 |
+
|
| 21 |
+
folder = os.path.expanduser(sys.argv[1])
|
| 22 |
+
URL = "http://localhost:5000/analyze"
|
| 23 |
+
|
| 24 |
+
if not os.path.isdir(folder):
|
| 25 |
+
print(f"β Folder tidak ditemukan: {folder}")
|
| 26 |
+
sys.exit(1)
|
| 27 |
+
|
| 28 |
+
# Cari semua gambar
|
| 29 |
+
EXTS = ["jpg", "jpeg", "png", "webp"]
|
| 30 |
+
all_files = []
|
| 31 |
+
for ext in EXTS:
|
| 32 |
+
all_files.extend(glob.glob(os.path.join(folder, f"*.{ext}")))
|
| 33 |
+
all_files.extend(glob.glob(os.path.join(folder, f"*.{ext.upper()}")))
|
| 34 |
+
|
| 35 |
+
# Klasifikasi berdasarkan prefix nama file (r1.jpg β real, f1.jpg β fake)
|
| 36 |
+
pattern = re.compile(r"^([rf])(\d+)\.", re.IGNORECASE)
|
| 37 |
+
labeled_files = []
|
| 38 |
+
for fp in all_files:
|
| 39 |
+
name = os.path.basename(fp)
|
| 40 |
+
m = pattern.match(name)
|
| 41 |
+
if m:
|
| 42 |
+
prefix = m.group(1).lower()
|
| 43 |
+
true_label = "real" if prefix == "r" else "fake"
|
| 44 |
+
idx = int(m.group(2))
|
| 45 |
+
labeled_files.append((fp, true_label, idx))
|
| 46 |
+
|
| 47 |
+
if not labeled_files:
|
| 48 |
+
print(f"β Gak ada file dengan format r1.jpg / f1.jpg dst di {folder}")
|
| 49 |
+
print(" File yang ada di folder:")
|
| 50 |
+
for fp in sorted(all_files)[:10]:
|
| 51 |
+
print(f" {os.path.basename(fp)}")
|
| 52 |
+
sys.exit(1)
|
| 53 |
+
|
| 54 |
+
# Sort: real dulu (sesuai index), terus fake
|
| 55 |
+
labeled_files.sort(key=lambda x: (x[1], x[2]))
|
| 56 |
+
print(f"β
Ditemukan {len(labeled_files)} gambar berlabel di {folder}\n")
|
| 57 |
+
|
| 58 |
+
stats = Counter()
|
| 59 |
+
mistakes = []
|
| 60 |
+
current_label = None
|
| 61 |
+
|
| 62 |
+
for fp, true_label, idx in labeled_files:
|
| 63 |
+
# Print header tiap ganti kategori
|
| 64 |
+
if true_label != current_label:
|
| 65 |
+
count = sum(1 for _, t, _ in labeled_files if t == true_label)
|
| 66 |
+
print(f"\n=== {true_label.upper()} ({count} files) ===")
|
| 67 |
+
print(f"{'file':<25} {'pred':<6} {'p_fake':>9} {'ok'}")
|
| 68 |
+
print("-" * 55)
|
| 69 |
+
current_label = true_label
|
| 70 |
+
|
| 71 |
+
try:
|
| 72 |
+
with open(fp, "rb") as f:
|
| 73 |
+
r = requests.post(URL, files={"image": f}, timeout=30)
|
| 74 |
+
except requests.exceptions.ConnectionError:
|
| 75 |
+
print(f"\nβ Backend gak nyala di {URL}")
|
| 76 |
+
print(" Jalanin dulu: python app.py")
|
| 77 |
+
sys.exit(1)
|
| 78 |
+
|
| 79 |
+
if r.status_code != 200:
|
| 80 |
+
print(f" ERROR on {fp}: {r.text}")
|
| 81 |
+
continue
|
| 82 |
+
|
| 83 |
+
d = r.json()
|
| 84 |
+
pred = "fake" if d["is_fake"] else "real"
|
| 85 |
+
ok = pred == true_label
|
| 86 |
+
stats[(true_label, pred)] += 1
|
| 87 |
+
if not ok:
|
| 88 |
+
mistakes.append((fp, true_label, pred, d["p_fake"]))
|
| 89 |
+
mark = "OK" if ok else "WRONG"
|
| 90 |
+
name = os.path.basename(fp)[:23]
|
| 91 |
+
print(f"{name:<25} {pred:<6} {d['p_fake']:>8.2f}% {mark}")
|
| 92 |
+
|
| 93 |
+
# Summary
|
| 94 |
+
print("\n" + "=" * 55)
|
| 95 |
+
print("CONFUSION MATRIX")
|
| 96 |
+
print("=" * 55)
|
| 97 |
+
print(f"{'':<10} {'pred_real':>12} {'pred_fake':>12}")
|
| 98 |
+
print(f"{'true_real':<10} {stats[('real','real')]:>12} {stats[('real','fake')]:>12}")
|
| 99 |
+
print(f"{'true_fake':<10} {stats[('fake','real')]:>12} {stats[('fake','fake')]:>12}")
|
| 100 |
+
|
| 101 |
+
total = sum(stats.values())
|
| 102 |
+
correct = stats[("real","real")] + stats[("fake","fake")]
|
| 103 |
+
if total:
|
| 104 |
+
print(f"\nAccuracy: {correct}/{total} = {100*correct/total:.1f}%")
|
| 105 |
+
|
| 106 |
+
n_real = stats[("real","real")] + stats[("real","fake")]
|
| 107 |
+
n_fake = stats[("fake","real")] + stats[("fake","fake")]
|
| 108 |
+
if n_real:
|
| 109 |
+
print(f"Real recall: {stats[('real','real')]}/{n_real} = {100*stats[('real','real')]/n_real:.1f}%")
|
| 110 |
+
if n_fake:
|
| 111 |
+
print(f"Fake recall: {stats[('fake','fake')]}/{n_fake} = {100*stats[('fake','fake')]/n_fake:.1f}%")
|
| 112 |
+
|
| 113 |
+
if mistakes:
|
| 114 |
+
print(f"\n{len(mistakes)} MISCLASSIFIED:")
|
| 115 |
+
for fp, t, p, pf in mistakes:
|
| 116 |
+
print(f" {os.path.basename(fp):<25} true={t:<5} pred={p:<5} p_fake={pf:.2f}%")
|