File size: 7,599 Bytes
55ae7dd 84cddaa 55ae7dd 11cc0f4 55ae7dd 88283ca 55ae7dd 88283ca 55ae7dd 88283ca 55ae7dd 11cc0f4 55ae7dd 11cc0f4 55ae7dd | 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 | import streamlit as st
import numpy as np
import tensorflow as tf
import cv2
import io
from PIL import Image, ImageChops, ImageEnhance
from tensorflow.keras import models, layers
# ββ Configuration ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
IMG_SIZE = (224, 224)
ELA_QUALITY = 90
ELA_SCALE = 15
# ββ Forensic Utilities βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def compute_ela(original, quality=ELA_QUALITY, scale=ELA_SCALE):
original = original.convert('RGB')
buf = io.BytesIO()
original.save(buf, 'JPEG', quality=quality)
buf.seek(0)
compressed = Image.open(buf)
ela_image = ImageChops.difference(original, compressed)
ela_image = ImageEnhance.Brightness(ela_image).enhance(scale)
return ela_image
def get_gradcam(model, input_data):
# Dynamically find the last conv layer
last_conv_layer_name = None
for layer in reversed(model.layers):
if 'conv2d' in layer.name:
last_conv_layer_name = layer.name
break
if not last_conv_layer_name:
# Fallback to any layer with conv in name
for layer in reversed(model.layers):
if 'conv' in layer.name:
last_conv_layer_name = layer.name
break
grad_model = models.Model(
inputs=model.inputs,
outputs=[model.get_layer(last_conv_layer_name).output, model.output]
)
with tf.GradientTape() as tape:
last_conv_out, preds = grad_model(input_data)
class_channel = preds[:, 0]
grads = tape.gradient(class_channel, last_conv_out)
pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))
heatmap = last_conv_out[0] @ pooled_grads[..., tf.newaxis]
max_val = tf.math.reduce_max(heatmap)
if max_val == 0:
max_val = 1e-10
heatmap = tf.squeeze(tf.maximum(heatmap, 0) / max_val).numpy()
return heatmap
def build_model(model_type='M3'):
# RGB branch β mirrors train.py get_rgb_branch() exactly
base = tf.keras.applications.ResNet50(include_top=False, weights='imagenet', input_shape=(*IMG_SIZE, 3))
base.trainable = False
rgb_input = layers.Input(shape=(*IMG_SIZE, 3))
x = tf.keras.applications.resnet50.preprocess_input(rgb_input)
x = base(x, training=False)
rgb_features = layers.GlobalAveragePooling2D()(x)
# ELA branch β mirrors train.py get_ela_branch() exactly (includes Rescaling)
ela_input = layers.Input(shape=(*IMG_SIZE, 3))
x = layers.Rescaling(1. / 255)(ela_input)
for filters in [32, 64, 128]:
x = layers.Conv2D(filters, (3, 3), activation='relu', padding='same')(x)
x = layers.BatchNormalization()(x)
x = layers.MaxPooling2D((2, 2))(x)
ela_features = layers.GlobalAveragePooling2D()(x)
fused = layers.Concatenate()([rgb_features, ela_features])
out = layers.Dense(1, activation='sigmoid')(
layers.Dropout(0.5)(layers.Dense(256, activation='relu')(fused))
)
return tf.keras.Model(inputs=[rgb_input, ela_input], outputs=out)
@st.cache_resource
def load_trained_model():
import os
from huggingface_hub import hf_hub_download
local_path = 'M3_best.h5'
# Resolve a path to the H5 weights (local first, then HF Hub)
if os.path.exists(local_path):
model_path = local_path
else:
st.info("Downloading model from Hugging Face Hub...")
model_path = hf_hub_download(
repo_id="usamaalam/image-forgery-detection-model",
filename="M3_best.h5",
cache_dir=".cache"
)
# Preferred: load the full saved model (architecture + weights) from H5
try:
model = tf.keras.models.load_model(model_path, compile=False)
st.success("Model loaded successfully!")
return model
except Exception as e:
st.warning(f"Full-model load failed ({e}); rebuilding architecture and loading weights...")
# Fallback: rebuild architecture and load weights by name
try:
model = build_model('M3')
model.load_weights(model_path, by_name=True, skip_mismatch=True)
st.success("Model loaded (weights-only fallback).")
return model
except Exception as e:
st.error(f"Failed to load model: {e}")
return None
# ββ Main UI ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
st.set_page_config(page_title="Image Forgery Detector", layout="wide")
st.title("π‘οΈ Image Forgery Detector")
st.markdown("""
Detect tampering in images using a Dual-Branch CNN (RGB + ELA).
Upload an image to see if it's Authentic or Forged.
""")
uploaded_file = st.file_uploader("Choose an image...", type=["jpg", "jpeg", "png", "tif"])
if uploaded_file is not None:
image = Image.open(uploaded_file).convert('RGB')
col1, col2 = st.columns(2)
with col1:
st.image(image, caption="Original Image", use_column_width=True)
with st.spinner("Analyzing..."):
# Load model
m3 = load_trained_model()
# RGB: preprocess_input handles normalization inside the branch
# ELA: Rescaling(1/255) is inside the branch, so pass raw [0,255]
rgb_in = np.array(image.resize(IMG_SIZE)).astype(np.float32)[np.newaxis]
ela_img = compute_ela(image)
ela_in = np.array(ela_img.resize(IMG_SIZE)).astype(np.float32)[np.newaxis]
input_data = [rgb_in, ela_in]
# Inference
pred = m3.predict(input_data, verbose=0)[0][0]
label = "FORGED" if pred > 0.5 else "AUTHENTIC"
confidence = pred if pred > 0.5 else 1 - pred
if 0.45 <= pred <= 0.55:
label = "UNCERTAIN"
with col2:
st.subheader("Prediction Result")
color = "red" if label == "FORGED" else "green" if label == "AUTHENTIC" else "orange"
st.markdown(f"### Result: <span style='color:{color}'>{label}</span>", unsafe_allow_html=True)
st.write(f"**Confidence:** {confidence:.2%}")
st.progress(float(confidence))
st.divider()
col3, col4 = st.columns(2)
with col3:
st.subheader("ELA Artifacts")
st.image(ela_img, caption="Error Level Analysis (JPEG inconsistencies)", use_column_width=True)
st.info("ELA highlights regions with different compression levels, often indicating tampered areas.")
with col4:
st.subheader("Grad-CAM Explainability")
try:
heatmap = get_gradcam(m3, input_data)
heatmap_color = cv2.applyColorMap(np.uint8(255 * heatmap), cv2.COLORMAP_JET)
heatmap_color = cv2.cvtColor(heatmap_color, cv2.COLOR_BGR2RGB)
heatmap_resized = cv2.resize(heatmap_color, (image.size[0], image.size[1]))
# Blend
img_np = np.array(image)
overlay = np.uint8(heatmap_resized * 0.4 + img_np * 0.6)
st.image(overlay, caption="Model Focus Regions", use_column_width=True)
st.info("The heatmap shows which parts of the image the model focused on to make its decision.")
except Exception as e:
st.error(f"Could not generate Grad-CAM: {e}")
else:
st.info("Please upload an image to start detection.")
|