salmanzaman777 commited on
Commit
4ad15a3
Β·
0 Parent(s):

feat: Add full implementation and HF Space artifacts

Browse files
.gitattributes ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ *.keras filter=lfs diff=lfs merge=lfs -text
2
+ *.ipynb filter=lfs diff=lfs merge=lfs -text
3
+ *.docx filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Local IDE / Claude Code ---
2
+ .claude/
3
+ .vscode/
4
+ .idea/
5
+
6
+ # --- HTML build artefact (rebuilt by pandoc; not source) ---
7
+ Documents/header_inline.html
8
+
9
+ # --- Future: data files (see PROJECT_SCOPE.md Β§5.2 + Β§6) ---
10
+ data/raw/
11
+ data/processed/
12
+
13
+ # --- Future: training outputs (weights live on HuggingFace Hub) ---
14
+ checkpoints/
15
+ logs/
16
+
17
+ # --- Python ---
18
+ __pycache__/
19
+ *.py[cod]
20
+ *$py.class
21
+ *.so
22
+ .Python
23
+ .venv/
24
+ venv/
25
+ env/
26
+ *.egg-info/
27
+ .pytest_cache/
28
+ .ruff_cache/
29
+
30
+ # --- Jupyter ---
31
+ .ipynb_checkpoints/
32
+
33
+ # --- OS ---
34
+ .DS_Store
35
+ Thumbs.db
36
+
37
+ # --- Secrets / credentials ---
38
+ .env
39
+ .env.local
40
+ *.pem
41
+ kaggle.json
Documents/Project_Report_Digital_Image_Forgery_Detector.docx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7744b5d6ae0dbb487a8093db92798e1b4732788f6a4ac713ee4e0c4bc7fe9a6c
3
+ size 23701
Image_Forgery_Detection_Colab_1.ipynb ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:565d9e04f3f6789367bcbcc038973f344e6ba9e1cc1b9c55d73d31a8dd92e409
3
+ size 37939
M3_best.keras ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b121144d425497c30e6d1a2a85acd6c5240069d642a9d8121567763bac0ec957
3
+ size 102889096
README.md ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Image Forgery Detector
3
+ emoji: πŸ›‘οΈ
4
+ colorFrom: blue
5
+ colorTo: red
6
+ sdk: streamlit
7
+ sdk_version: 1.35.0
8
+ python_version: 3.11
9
+ app_file: app.py
10
+ pinned: false
11
+ ---
12
+
13
+ # Image Forgery Detector
14
+
15
+ This application detects tampering in images using a Dual-Branch CNN architecture.
16
+
17
+ ## How it works:
18
+ 1. **RGB Branch:** Uses a pretrained ResNet50 to extract semantic features from the original image.
19
+ 2. **ELA Branch:** Computes Error Level Analysis (ELA) to detect JPEG compression inconsistencies.
20
+ 3. **Fused Model:** Combines features from both branches to make a final prediction.
21
+
22
+ ## Explainability:
23
+ The app uses **Grad-CAM** to visualize which parts of the image the model focused on when making its decision.
24
+
25
+ ## Deployment:
26
+ This app is designed to be deployed on Hugging Face Spaces using Streamlit.
app.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import numpy as np
3
+ import tensorflow as tf
4
+ import cv2
5
+ import io
6
+ from PIL import Image, ImageChops
7
+ from tensorflow.keras import models
8
+
9
+ # ── Configuration ────────────────────────────────────────────────────────────
10
+ IMG_SIZE = (224, 224)
11
+ ELA_QUALITY = 90
12
+ ELA_SCALE = 15
13
+
14
+ # ── Forensic Utilities ───────────────────────────────────────────────────────
15
+ def compute_ela(original, quality=ELA_QUALITY, scale=ELA_SCALE):
16
+ original = original.convert('RGB')
17
+ buf = io.BytesIO()
18
+ original.save(buf, 'JPEG', quality=quality)
19
+ buf.seek(0)
20
+ compressed = Image.open(buf)
21
+
22
+ ela_image = ImageChops.difference(original, compressed)
23
+ ela_image = ImageChops.multiply(
24
+ ela_image, Image.new('RGB', ela_image.size, (scale, scale, scale))
25
+ )
26
+ return ela_image
27
+
28
+ def get_gradcam(model, input_data):
29
+ # Dynamically find the last conv layer
30
+ last_conv_layer_name = None
31
+ for layer in reversed(model.layers):
32
+ if 'conv2d' in layer.name:
33
+ last_conv_layer_name = layer.name
34
+ break
35
+
36
+ if not last_conv_layer_name:
37
+ # Fallback to any layer with conv in name
38
+ for layer in reversed(model.layers):
39
+ if 'conv' in layer.name:
40
+ last_conv_layer_name = layer.name
41
+ break
42
+
43
+ grad_model = models.Model(
44
+ inputs=model.inputs,
45
+ outputs=[model.get_layer(last_conv_layer_name).output, model.output]
46
+ )
47
+
48
+ with tf.GradientTape() as tape:
49
+ last_conv_out, preds = grad_model(input_data)
50
+ class_channel = preds[:, 0]
51
+
52
+ grads = tape.gradient(class_channel, last_conv_out)
53
+ pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))
54
+ heatmap = last_conv_out[0] @ pooled_grads[..., tf.newaxis]
55
+
56
+ max_val = tf.math.reduce_max(heatmap)
57
+ if max_val == 0:
58
+ max_val = 1e-10
59
+ heatmap = tf.squeeze(tf.maximum(heatmap, 0) / max_val).numpy()
60
+ return heatmap
61
+
62
+ @st.cache_resource
63
+ def load_trained_model():
64
+ return models.load_model('M3_best.keras')
65
+
66
+ # ── Main UI ──────────────────────────────────────────────────────────────────
67
+ st.set_page_config(page_title="Image Forgery Detector", layout="wide")
68
+
69
+ st.title("πŸ›‘οΈ Image Forgery Detector")
70
+ st.markdown("""
71
+ Detect tampering in images using a Dual-Branch CNN (RGB + ELA).
72
+ Upload an image to see if it's Authentic or Forged.
73
+ """)
74
+
75
+ uploaded_file = st.file_uploader("Choose an image...", type=["jpg", "jpeg", "png", "tif"])
76
+
77
+ if uploaded_file is not None:
78
+ image = Image.open(uploaded_file).convert('RGB')
79
+
80
+ col1, col2 = st.columns(2)
81
+ with col1:
82
+ st.image(image, caption="Original Image", use_column_width=True)
83
+
84
+ with st.spinner("Analyzing..."):
85
+ # Load model
86
+ m3 = load_trained_model()
87
+
88
+ # Prepare inputs
89
+ rgb_in = np.array(image.resize(IMG_SIZE)).astype(np.float32)[np.newaxis, ...]
90
+ ela_img = compute_ela(image)
91
+ ela_in = np.array(ela_img.resize(IMG_SIZE)).astype(np.float32)[np.newaxis, ...]
92
+
93
+ # Handle input mapping
94
+ try:
95
+ if hasattr(m3, 'input_names') and m3.input_names:
96
+ input_data = {name: tensor for name, tensor in zip(m3.input_names, [rgb_in, ela_in])}
97
+ else:
98
+ input_data = [rgb_in, ela_in]
99
+ except:
100
+ input_data = [rgb_in, ela_in]
101
+
102
+ # Inference
103
+ pred = m3.predict(input_data, verbose=0)[0][0]
104
+ label = "FORGED" if pred > 0.5 else "AUTHENTIC"
105
+ confidence = pred if pred > 0.5 else 1 - pred
106
+
107
+ if 0.45 <= pred <= 0.55:
108
+ label = "UNCERTAIN"
109
+
110
+ with col2:
111
+ st.subheader("Prediction Result")
112
+ color = "red" if label == "FORGED" else "green" if label == "AUTHENTIC" else "orange"
113
+ st.markdown(f"### Result: <span style='color:{color}'>{label}</span>", unsafe_allow_html=True)
114
+ st.write(f"**Confidence:** {confidence:.2%}")
115
+
116
+ st.progress(float(confidence))
117
+
118
+ st.divider()
119
+
120
+ col3, col4 = st.columns(2)
121
+ with col3:
122
+ st.subheader("ELA Artifacts")
123
+ st.image(ela_img, caption="Error Level Analysis (JPEG inconsistencies)", use_column_width=True)
124
+ st.info("ELA highlights regions with different compression levels, often indicating tampered areas.")
125
+
126
+ with col4:
127
+ st.subheader("Grad-CAM Explainability")
128
+ try:
129
+ heatmap = get_gradcam(m3, input_data)
130
+ heatmap_color = cv2.applyColorMap(np.uint8(255 * heatmap), cv2.COLORMAP_JET)
131
+ heatmap_color = cv2.cvtColor(heatmap_color, cv2.COLOR_BGR2RGB)
132
+ heatmap_resized = cv2.resize(heatmap_color, (image.size[0], image.size[1]))
133
+
134
+ # Blend
135
+ img_np = np.array(image)
136
+ overlay = np.uint8(heatmap_resized * 0.4 + img_np * 0.6)
137
+ st.image(overlay, caption="Model Focus Regions", use_column_width=True)
138
+ st.info("The heatmap shows which parts of the image the model focused on to make its decision.")
139
+ except Exception as e:
140
+ st.error(f"Could not generate Grad-CAM: {e}")
141
+
142
+ else:
143
+ st.info("Please upload an image to start detection.")
packages.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ git
2
+ git-lfs
3
+ ffmpeg
4
+ libsm6
5
+ libxext6
6
+ cmake
7
+ rsync
8
+ libgl1
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ streamlit
2
+ tensorflow
3
+ opencv-python-headless
4
+ pillow
5
+ numpy
6
+ scikit-learn
7
+ matplotlib
train.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import io
4
+ import random
5
+ import numpy as np
6
+ import tensorflow as tf
7
+ import cv2
8
+ from PIL import Image, ImageChops, ImageDraw
9
+ from sklearn.model_selection import train_test_split
10
+ from tensorflow.keras import layers, models, applications
11
+
12
+ # ── Global configuration ──────────────────────────────────────────────────────
13
+ SEED = 42
14
+ IMG_SIZE = (224, 224)
15
+ ELA_QUALITY = 90
16
+ ELA_SCALE = 15
17
+ BATCH_SIZE = 32
18
+ EPOCHS = 5
19
+ TARGET_DIR = "./casia_v2"
20
+
21
+ def set_reproducibility(seed=SEED):
22
+ tf.random.set_seed(seed)
23
+ np.random.seed(seed)
24
+ random.seed(seed)
25
+ os.environ['PYTHONHASHSEED'] = str(seed)
26
+
27
+ set_reproducibility()
28
+
29
+ def generate_robust_dataset(num_samples=120):
30
+ if os.path.exists(TARGET_DIR):
31
+ import shutil
32
+ shutil.rmtree(TARGET_DIR)
33
+ os.makedirs(TARGET_DIR)
34
+
35
+ print(f"Generating {num_samples} synthetic samples...")
36
+ for i in range(num_samples):
37
+ img_data = np.random.randint(100, 200, (256, 256, 3), dtype=np.uint8)
38
+ img = Image.fromarray(img_data)
39
+
40
+ is_forged = i >= (num_samples // 2)
41
+ if not is_forged:
42
+ filename = f"Au_arc_000{i:02d}.jpg"
43
+ else:
44
+ draw = ImageDraw.Draw(img)
45
+ draw.rectangle([50, 50, 150, 150], fill=(255, 0, 0))
46
+ filename = f"Tp_s_N_arc_000{i:02d}_00099_001.jpg"
47
+
48
+ img.save(os.path.join(TARGET_DIR, filename))
49
+
50
+ def compute_ela(image_path_or_pil, quality=ELA_QUALITY, scale=ELA_SCALE):
51
+ if isinstance(image_path_or_pil, str):
52
+ original = Image.open(image_path_or_pil).convert('RGB')
53
+ else:
54
+ original = image_path_or_pil.convert('RGB')
55
+
56
+ buf = io.BytesIO()
57
+ original.save(buf, 'JPEG', quality=quality)
58
+ buf.seek(0)
59
+ compressed = Image.open(buf)
60
+
61
+ ela_image = ImageChops.difference(original, compressed)
62
+ ela_image = ImageChops.multiply(
63
+ ela_image, Image.new('RGB', ela_image.size, (scale, scale, scale))
64
+ )
65
+ return ela_image
66
+
67
+ class CASIAParser:
68
+ @staticmethod
69
+ def get_ids(filename):
70
+ name = os.path.basename(filename)
71
+ if name.startswith('Au_'):
72
+ match = re.search(r'Au_[a-z]{3}_(\d+)', name)
73
+ return [match.group(1)] if match else []
74
+ elif name.startswith('Tp_'):
75
+ parts = name.split('_')
76
+ return [parts[4], parts[5]] if len(parts) >= 6 else []
77
+ return []
78
+
79
+ def split_dataset(data_dir, train_ratio=0.8, val_ratio=0.1, test_ratio=0.1):
80
+ all_images = [
81
+ os.path.join(data_dir, f)
82
+ for f in os.listdir(data_dir)
83
+ if f.lower().endswith(('.jpg', '.jpeg', '.png', '.tif'))
84
+ ]
85
+ unique_ids = sorted({i for p in all_images for i in CASIAParser.get_ids(p)})
86
+ if not unique_ids:
87
+ unique_ids = [str(i) for i in range(len(all_images))]
88
+ tr_ids, temp = train_test_split(unique_ids, train_size=train_ratio, random_state=SEED)
89
+ v_ids, _ = train_test_split(temp, train_size=val_ratio / (val_ratio + test_ratio), random_state=SEED)
90
+ tr_ids, v_ids = set(tr_ids), set(v_ids)
91
+ splits = {'train': [], 'val': [], 'test': []}
92
+ for p in all_images:
93
+ ids = CASIAParser.get_ids(p)
94
+ if not ids:
95
+ splits['train'].append(p) if random.random() < 0.8 else splits['test'].append(p)
96
+ continue
97
+ if any(i in tr_ids for i in ids): splits['train'].append(p)
98
+ elif any(i in v_ids for i in ids): splits['val'].append(p)
99
+ else: splits['test'].append(p)
100
+ return splits
101
+
102
+ def preload_images(paths, img_size=IMG_SIZE):
103
+ rgb_list, ela_list, label_list = [], [], []
104
+ for p in paths:
105
+ pil_img = Image.open(p).convert('RGB')
106
+ rgb_list.append(np.array(pil_img.resize(img_size), dtype=np.float32))
107
+ ela_list.append(np.array(compute_ela(pil_img).resize(img_size), dtype=np.float32))
108
+ label_list.append(1 if os.path.basename(p).startswith('Tp_') else 0)
109
+ return np.array(rgb_list), np.array(ela_list), np.array(label_list)
110
+
111
+ def make_dataset(rgb_arr, ela_arr, labels, batch_size=BATCH_SIZE, shuffle=False, repeat=True):
112
+ ds = tf.data.Dataset.from_tensor_slices(((rgb_arr, ela_arr), labels))
113
+ if shuffle:
114
+ ds = ds.shuffle(buffer_size=len(labels), seed=SEED, reshuffle_each_iteration=True)
115
+ ds = ds.batch(batch_size, drop_remainder=False)
116
+ if repeat:
117
+ ds = ds.repeat()
118
+ return ds.prefetch(tf.data.AUTOTUNE)
119
+
120
+ def get_rgb_branch():
121
+ base = applications.ResNet50(
122
+ include_top=False, weights='imagenet', input_shape=(*IMG_SIZE, 3)
123
+ )
124
+ base.trainable = False
125
+ inputs = layers.Input(shape=(*IMG_SIZE, 3))
126
+ x = applications.resnet50.preprocess_input(inputs)
127
+ x = base(x, training=False)
128
+ return inputs, layers.GlobalAveragePooling2D()(x)
129
+
130
+ def get_ela_branch():
131
+ inputs = layers.Input(shape=(*IMG_SIZE, 3))
132
+ x = layers.Rescaling(1. / 255)(inputs)
133
+ for filters in [32, 64, 128]:
134
+ x = layers.Conv2D(filters, (3, 3), activation='relu', padding='same')(x)
135
+ x = layers.BatchNormalization()(x)
136
+ x = layers.MaxPooling2D((2, 2))(x)
137
+ return inputs, layers.GlobalAveragePooling2D()(x)
138
+
139
+ def build_model():
140
+ rgb_in, rgb_f = get_rgb_branch()
141
+ ela_in, ela_f = get_ela_branch()
142
+ fused = layers.Concatenate()([rgb_f, ela_f])
143
+ out = layers.Dense(1, activation='sigmoid')(
144
+ layers.Dropout(0.5)(layers.Dense(256, activation='relu')(fused))
145
+ )
146
+ return models.Model(inputs=[rgb_in, ela_in], outputs=out)
147
+
148
+ if __name__ == "__main__":
149
+ generate_robust_dataset(120)
150
+ splits = split_dataset(TARGET_DIR)
151
+ train_rgb, train_ela, train_labels = preload_images(splits['train'])
152
+ val_rgb, val_ela, val_labels = preload_images(splits['val'])
153
+
154
+ train_ds = make_dataset(train_rgb, train_ela, train_labels, shuffle=True)
155
+ val_ds = make_dataset(val_rgb, val_ela, val_labels, shuffle=False)
156
+
157
+ model = build_model()
158
+ model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
159
+
160
+ steps_per_epoch = max(1, int(np.ceil(len(train_labels) / BATCH_SIZE)))
161
+ validation_steps = max(1, int(np.ceil(len(val_labels) / BATCH_SIZE)))
162
+
163
+ model.fit(
164
+ train_ds,
165
+ validation_data=val_ds,
166
+ epochs=EPOCHS,
167
+ steps_per_epoch=steps_per_epoch,
168
+ validation_steps=validation_steps,
169
+ verbose=1,
170
+ )
171
+ model.save('M3_best.keras')
172
+ print("Model saved as M3_best.keras")