usamaalam Claude Sonnet 4.6 commited on
Commit
55ae7dd
Β·
0 Parent(s):

Clean deployment: model loaded from HF Hub at runtime

Browse files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

.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,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
42
+ model/*.h5
43
+ model/*.keras
CLAUDE.md ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ **Install dependencies:**
8
+ ```bash
9
+ pip install -r requirements.txt
10
+ ```
11
+
12
+ **Run the Streamlit app locally:**
13
+ ```bash
14
+ streamlit run app.py
15
+ ```
16
+
17
+ **Train the model (generates synthetic data and saves `model/M3_best.keras`):**
18
+ ```bash
19
+ python train.py
20
+ ```
21
+
22
+ The trained model file `model/M3_best.keras` is tracked via Git LFS (~98 MB).
23
+
24
+ ## Architecture
25
+
26
+ This is a two-file ML project: a training script and a Streamlit inference app.
27
+
28
+ ### Model: Dual-Branch CNN (`train.py`)
29
+
30
+ The model takes two inputs for every image:
31
+
32
+ - **RGB branch** β€” frozen ResNet50 (pretrained on ImageNet) β†’ `GlobalAveragePooling2D`. Captures semantic/texture features.
33
+ - **ELA branch** β€” custom 3-block CNN (Conv2D β†’ BatchNorm β†’ MaxPool, filters: 32β†’64β†’128) β†’ `GlobalAveragePooling2D`. Operates on the Error Level Analysis image.
34
+
35
+ Both branch outputs are concatenated β†’ Dense(256, relu) β†’ Dropout(0.5) β†’ Dense(1, sigmoid). Binary output: 0 = Authentic, 1 = Forged.
36
+
37
+ **ELA** (Error Level Analysis): re-saves the image as JPEG at `quality=90`, diffs it against the original, then amplifies the difference by `scale=15`. Tampered regions show higher residuals due to compression inconsistency.
38
+
39
+ **Dataset convention (CASIA v2 naming):**
40
+ - Authentic files: `Au_<type>_<id>.jpg`
41
+ - Tampered files: `Tp_s_N_<type>_<donor_id>_<tampered_id>_<seq>.jpg`
42
+
43
+ `CASIAParser` extracts IDs from filenames to ensure donor/tampered image pairs stay in the same split (prevents data leakage across train/val/test).
44
+
45
+ ### Inference App (`app.py`)
46
+
47
+ Loads `model/M3_best.keras` (cached via `@st.cache_resource`). For each uploaded image:
48
+ 1. Computes ELA image using identical parameters as training (`quality=90`, `scale=15`).
49
+ 2. Resizes both RGB and ELA to `(224, 224)` and runs `model.predict`.
50
+ 3. Threshold: `pred > 0.5` β†’ FORGED, `pred < 0.5` β†’ AUTHENTIC, `0.45–0.55` β†’ UNCERTAIN.
51
+ 4. Generates a **Grad-CAM** heatmap by dynamically finding the last `conv2d` layer, computing gradients of the output w.r.t. that layer's activations, and overlaying a JET colormap on the original image.
52
+
53
+ ### Deployment
54
+
55
+ Configured for **Hugging Face Spaces** (Streamlit SDK). The `README.md` frontmatter contains the Space metadata. `packages.txt` installs system-level dependencies needed for OpenCV headless (`libgl1`, `libsm6`, `libxext6`) and Git LFS.
README.md ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ πŸš€ **Live on Hugging Face Spaces:** [image-forgery-detector](https://huggingface.co/spaces/usamaalam/image-forgery-detector)
27
+
28
+ ## Repository:
29
+ - **GitHub:** [https://github.com/salmanzaman777/image-forgery-detector](https://github.com/salmanzaman777/image-forgery-detector)
30
+ - **Branch:** `usama` (latest with M3 model trained on CASIA v2)
31
+
32
+ ## Documents:
33
+ - [Project Report](documents/Project_Report_Digital_Image_Forgery_Detector.docx)
app.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, ImageEnhance
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 = ImageEnhance.Brightness(ela_image).enhance(scale)
24
+ return ela_image
25
+
26
+ def get_gradcam(model, input_data):
27
+ # Dynamically find the last conv layer
28
+ last_conv_layer_name = None
29
+ for layer in reversed(model.layers):
30
+ if 'conv2d' in layer.name:
31
+ last_conv_layer_name = layer.name
32
+ break
33
+
34
+ if not last_conv_layer_name:
35
+ # Fallback to any layer with conv in name
36
+ for layer in reversed(model.layers):
37
+ if 'conv' in layer.name:
38
+ last_conv_layer_name = layer.name
39
+ break
40
+
41
+ grad_model = models.Model(
42
+ inputs=model.inputs,
43
+ outputs=[model.get_layer(last_conv_layer_name).output, model.output]
44
+ )
45
+
46
+ with tf.GradientTape() as tape:
47
+ last_conv_out, preds = grad_model(input_data)
48
+ class_channel = preds[:, 0]
49
+
50
+ grads = tape.gradient(class_channel, last_conv_out)
51
+ pooled_grads = tf.reduce_mean(grads, axis=(0, 1, 2))
52
+ heatmap = last_conv_out[0] @ pooled_grads[..., tf.newaxis]
53
+
54
+ max_val = tf.math.reduce_max(heatmap)
55
+ if max_val == 0:
56
+ max_val = 1e-10
57
+ heatmap = tf.squeeze(tf.maximum(heatmap, 0) / max_val).numpy()
58
+ return heatmap
59
+
60
+ def build_model(model_type='M3'):
61
+ IMG_SIZE = (224, 224)
62
+ ela_input = layers.Input(shape=(*IMG_SIZE, 3), name='ela_input')
63
+ x = layers.Conv2D(32, (3,3), activation='relu', padding='same')(ela_input)
64
+ x = layers.BatchNormalization()(x)
65
+ x = layers.MaxPooling2D()(x)
66
+ x = layers.Conv2D(64, (3,3), activation='relu', padding='same')(x)
67
+ x = layers.BatchNormalization()(x)
68
+ x = layers.MaxPooling2D()(x)
69
+ x = layers.Conv2D(128, (3,3), activation='relu', padding='same')(x)
70
+ x = layers.BatchNormalization()(x)
71
+ x = layers.MaxPooling2D()(x)
72
+ ela_features = layers.GlobalAveragePooling2D(name='ela_gap')(x)
73
+
74
+ rgb_input = layers.Input(shape=(*IMG_SIZE, 3), name='rgb_input')
75
+ resnet = tf.keras.applications.ResNet50(weights='imagenet', include_top=False, input_tensor=rgb_input)
76
+ for layer in resnet.layers:
77
+ layer.trainable = False
78
+ rgb_features = layers.GlobalAveragePooling2D(name='rgb_gap')(resnet.output)
79
+
80
+ combined = layers.Concatenate(name='fused')([rgb_features, ela_features])
81
+ x = layers.Dense(256, activation='relu')(combined)
82
+ x = layers.Dropout(0.5)(x)
83
+ output = layers.Dense(1, activation='sigmoid', name='output')(x)
84
+
85
+ return tf.keras.Model(inputs=[rgb_input, ela_input], outputs=output, name=model_type)
86
+
87
+ @st.cache_resource
88
+ def load_trained_model():
89
+ import os
90
+ from huggingface_hub import hf_hub_download
91
+
92
+ model_path = 'M3_best.h5'
93
+
94
+ # Try local file first
95
+ if os.path.exists(model_path):
96
+ try:
97
+ model = build_model('M3')
98
+ model.load_weights(model_path)
99
+ return model
100
+ except Exception as e:
101
+ st.warning(f"Local model load failed: {e}. Downloading from HF Hub...")
102
+
103
+ # Download from HF Model Hub
104
+ try:
105
+ st.info("Downloading model from Hugging Face Hub...")
106
+ model_path = hf_hub_download(
107
+ repo_id="usamaalam/image-forgery-detection-model",
108
+ filename="M3_best.h5",
109
+ cache_dir=".cache"
110
+ )
111
+ model = build_model('M3')
112
+ model.load_weights(model_path)
113
+ st.success("Model loaded successfully!")
114
+ return model
115
+ except Exception as e:
116
+ st.error(f"Failed to load model: {e}")
117
+ return None
118
+
119
+ # ── Main UI ──────────────────────────────────────────────────────────────────
120
+ st.set_page_config(page_title="Image Forgery Detector", layout="wide")
121
+
122
+ st.title("πŸ›‘οΈ Image Forgery Detector")
123
+ st.markdown("""
124
+ Detect tampering in images using a Dual-Branch CNN (RGB + ELA).
125
+ Upload an image to see if it's Authentic or Forged.
126
+ """)
127
+
128
+ uploaded_file = st.file_uploader("Choose an image...", type=["jpg", "jpeg", "png", "tif"])
129
+
130
+ if uploaded_file is not None:
131
+ image = Image.open(uploaded_file).convert('RGB')
132
+
133
+ col1, col2 = st.columns(2)
134
+ with col1:
135
+ st.image(image, caption="Original Image", use_column_width=True)
136
+
137
+ with st.spinner("Analyzing..."):
138
+ # Load model
139
+ m3 = load_trained_model()
140
+
141
+ # Prepare inputs β€” normalize to [0, 1] to match training
142
+ rgb_in = np.array(image.resize(IMG_SIZE)).astype(np.float32)[np.newaxis] / 255.0
143
+ ela_img = compute_ela(image)
144
+ ela_in = np.array(ela_img.resize(IMG_SIZE)).astype(np.float32)[np.newaxis] / 255.0
145
+ input_data = [rgb_in, ela_in]
146
+
147
+ # Inference
148
+ pred = m3.predict(input_data, verbose=0)[0][0]
149
+ label = "FORGED" if pred > 0.5 else "AUTHENTIC"
150
+ confidence = pred if pred > 0.5 else 1 - pred
151
+
152
+ if 0.45 <= pred <= 0.55:
153
+ label = "UNCERTAIN"
154
+
155
+ with col2:
156
+ st.subheader("Prediction Result")
157
+ color = "red" if label == "FORGED" else "green" if label == "AUTHENTIC" else "orange"
158
+ st.markdown(f"### Result: <span style='color:{color}'>{label}</span>", unsafe_allow_html=True)
159
+ st.write(f"**Confidence:** {confidence:.2%}")
160
+
161
+ st.progress(float(confidence))
162
+
163
+ st.divider()
164
+
165
+ col3, col4 = st.columns(2)
166
+ with col3:
167
+ st.subheader("ELA Artifacts")
168
+ st.image(ela_img, caption="Error Level Analysis (JPEG inconsistencies)", use_column_width=True)
169
+ st.info("ELA highlights regions with different compression levels, often indicating tampered areas.")
170
+
171
+ with col4:
172
+ st.subheader("Grad-CAM Explainability")
173
+ try:
174
+ heatmap = get_gradcam(m3, input_data)
175
+ heatmap_color = cv2.applyColorMap(np.uint8(255 * heatmap), cv2.COLORMAP_JET)
176
+ heatmap_color = cv2.cvtColor(heatmap_color, cv2.COLOR_BGR2RGB)
177
+ heatmap_resized = cv2.resize(heatmap_color, (image.size[0], image.size[1]))
178
+
179
+ # Blend
180
+ img_np = np.array(image)
181
+ overlay = np.uint8(heatmap_resized * 0.4 + img_np * 0.6)
182
+ st.image(overlay, caption="Model Focus Regions", use_column_width=True)
183
+ st.info("The heatmap shows which parts of the image the model focused on to make its decision.")
184
+ except Exception as e:
185
+ st.error(f"Could not generate Grad-CAM: {e}")
186
+
187
+ else:
188
+ st.info("Please upload an image to start detection.")
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
documents/REMEDIATION_PLAN.md ADDED
@@ -0,0 +1,450 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Remediation & Enhancement Plan
2
+ ## Image Forgery Detector β€” PGD Student Project
3
+
4
+ This document lists every gap and issue found during validation, with step-by-step actions to fix each one. Work through the sections **in priority order** (Critical β†’ High β†’ Medium β†’ Low). Do not skip ahead β€” later fixes depend on earlier ones being done first.
5
+
6
+ ---
7
+
8
+ ## Quick Reference: Issue Summary
9
+
10
+ | # | Severity | Issue | Files Affected |
11
+ |---|----------|-------|----------------|
12
+ | 1 | CRITICAL | `run_training()` never defined in notebook | `.ipynb` |
13
+ | 2 | CRITICAL | Synthetic toy data β€” model learns nothing real | `.ipynb`, `train.py` |
14
+ | 3 | HIGH | Notebook section order is broken (Β§9 β†’ Β§7 β†’ Β§8) | `.ipynb` |
15
+ | 4 | HIGH | No meaningful evaluation metrics (only accuracy) | `.ipynb` |
16
+ | 5 | HIGH | Ablation study conclusion is invalid (all 100%) | `.ipynb` |
17
+ | 6 | MEDIUM | `train.py` only trains M3, not M1/M2 | `train.py` |
18
+ | 7 | MEDIUM | Project report document missing from repo | `Documents/` |
19
+ | 8 | MEDIUM | No literature comparison or baseline results | `.ipynb` |
20
+ | 9 | LOW | `get_gradcam()` in app.py is simplified vs notebook | `app.py` |
21
+ | 10 | LOW | Gradio vs Streamlit discrepancy not documented | `README.md` |
22
+
23
+ ---
24
+
25
+ ## CRITICAL FIXES
26
+
27
+ ---
28
+
29
+ ### Fix 1 β€” Add the Missing `run_training()` Function to the Notebook
30
+
31
+ **Problem:** Cell 16 of the notebook calls `run_training('M1', ...)`, `run_training('M2', ...)`, and `run_training('M3', ...)`, but this function is never defined in any visible cell. The notebook **cannot be run end-to-end** as currently submitted. An examiner who tries to run it will get a `NameError`.
32
+
33
+ **Why this matters:** A Colab notebook is expected to be fully self-contained. Every function that is called must be defined above the call site.
34
+
35
+ **Steps to fix:**
36
+
37
+ 1. Open `Image_Forgery_Detection_Colab_1.ipynb` in Google Colab.
38
+ 2. Insert a new code cell **between the `build_model()` cell and the ablation execution cell** (between the current cell-9 and cell-16).
39
+ 3. Add the following function to that cell:
40
+
41
+ ```python
42
+ def run_training(model_type, train_ds_base, val_ds_base, n_train, n_val):
43
+ print(f"\n{'='*55}")
44
+ print(f"Training {model_type}")
45
+ print(f"{'='*55}")
46
+
47
+ train_ds = adapt_dataset_for_model(train_ds_base, model_type)
48
+ val_ds = adapt_dataset_for_model(val_ds_base, model_type)
49
+
50
+ model = build_model(model_type)
51
+ model.compile(
52
+ optimizer='adam',
53
+ loss='binary_crossentropy',
54
+ metrics=['accuracy']
55
+ )
56
+
57
+ steps_per_epoch = max(1, int(np.ceil(n_train / BATCH_SIZE)))
58
+ validation_steps = max(1, int(np.ceil(n_val / BATCH_SIZE)))
59
+
60
+ history = model.fit(
61
+ train_ds,
62
+ validation_data=val_ds,
63
+ epochs=EPOCHS,
64
+ steps_per_epoch=steps_per_epoch,
65
+ validation_steps=validation_steps,
66
+ verbose=1,
67
+ )
68
+
69
+ save_path = f"{model_type}_best.keras"
70
+ model.save(save_path)
71
+ print(f"βœ” {model_type} saved β†’ {save_path}")
72
+ return model, history
73
+ ```
74
+
75
+ 4. Run all cells from top to bottom to confirm there are no errors.
76
+ 5. The notebook output should show three training runs (M1, M2, M3) completing without a `NameError`.
77
+
78
+ ---
79
+
80
+ ### Fix 2 β€” Replace Synthetic Toy Data with Real CASIA v2
81
+
82
+ **Problem:** The current dataset is:
83
+ - **Authentic**: random noise pixels (RGB 100–200), no real photographic content
84
+ - **Forged**: same random noise + a solid red rectangle at fixed coordinates [50–150, 50–150]
85
+
86
+ The model trivially learns "red rectangle = forged" and achieves 100% accuracy. This has **no relationship to real image forgery detection**. The architecture, ELA, and Grad-CAM are all correctly implemented β€” but without real data, none of it is being tested.
87
+
88
+ **Why this matters:** An examiner will immediately recognise that 100% accuracy on 120 synthetic images is not a valid result. It is the single biggest weakness in the submission.
89
+
90
+ **Steps to fix:**
91
+
92
+ #### Step 2a β€” Download CASIA v2
93
+
94
+ 1. Go to Kaggle and search for **"CASIA v2 image forgery"** or **"CASIA 2.0 dataset"**.
95
+ 2. Download the dataset (approximately 3.3 GB). It contains:
96
+ - ~12,614 authentic images (`Au_*.jpg`, `Au_*.tif`, etc.)
97
+ - ~5,123 tampered images (`Tp_*.jpg`, `Tp_*.tif`, etc.)
98
+ 3. Upload to your **Google Drive** in a folder named `casia_v2/`.
99
+
100
+ #### Step 2b β€” Mount Drive and Point Training to Real Data
101
+
102
+ 1. In the notebook, add a cell at the very beginning of the data section:
103
+
104
+ ```python
105
+ from google.colab import drive
106
+ drive.mount('/content/drive')
107
+
108
+ TARGET_DIR = "/content/drive/MyDrive/casia_v2" # adjust path if needed
109
+ ```
110
+
111
+ 2. Remove or comment out the call to `generate_robust_dataset()` β€” you no longer need synthetic data.
112
+ 3. Run `split_dataset(TARGET_DIR)` directly on the real data.
113
+
114
+ #### Step 2c β€” Verify the Split is Correct
115
+
116
+ After splitting, print and confirm the numbers look reasonable:
117
+
118
+ ```python
119
+ splits = split_dataset(TARGET_DIR)
120
+ print(f"Train: {len(splits['train'])} | Val: {len(splits['val'])} | Test: {len(splits['test'])}")
121
+
122
+ # Also check label distribution
123
+ for split_name, paths in splits.items():
124
+ authentic = sum(1 for p in paths if os.path.basename(p).startswith('Au_'))
125
+ forged = sum(1 for p in paths if os.path.basename(p).startswith('Tp_'))
126
+ print(f"{split_name}: {authentic} authentic, {forged} forged")
127
+ ```
128
+
129
+ Expected output (approximate):
130
+ ```
131
+ Train: ~14,000 | Val: ~1,700 | Test: ~1,700
132
+ train: ~10,000 authentic, ~4,000 forged
133
+ ```
134
+
135
+ #### Step 2d β€” Note on Class Imbalance
136
+
137
+ CASIA v2 has roughly 2.5Γ— more authentic than tampered images. Update the model compilation to handle this:
138
+
139
+ ```python
140
+ # Add class_weight to model.fit to handle imbalance
141
+ from sklearn.utils.class_weight import compute_class_weight
142
+
143
+ all_labels = train_labels # the label array from preload_images
144
+ classes = np.unique(all_labels)
145
+ weights = compute_class_weight('balanced', classes=classes, y=all_labels)
146
+ class_weight_dict = dict(zip(classes, weights))
147
+ print("Class weights:", class_weight_dict)
148
+ ```
149
+
150
+ Pass `class_weight=class_weight_dict` to `model.fit()`.
151
+
152
+ #### Step 2e β€” Re-train and Save Model
153
+
154
+ Re-run training. The accuracy will no longer be 100%. Expect:
155
+ - A reasonable result is **80–92% accuracy** on real CASIA v2 with this architecture
156
+ - If accuracy is above 95%, double-check for data leakage
157
+ - If accuracy is below 70%, consider increasing EPOCHS to 10–15
158
+
159
+ Save the newly trained M3 to `M3_best.keras` and download it from Colab:
160
+
161
+ ```python
162
+ from google.colab import files
163
+ files.download('M3_best.keras')
164
+ ```
165
+
166
+ Then replace the existing `M3_best.keras` in the repo with the newly trained file (Git LFS will handle the upload).
167
+
168
+ ---
169
+
170
+ ## HIGH PRIORITY FIXES
171
+
172
+ ---
173
+
174
+ ### Fix 3 β€” Reorder Notebook Sections
175
+
176
+ **Problem:** The notebook section headings appear in the wrong order:
177
+ - Cell-10 is labelled **Β§9 Execute** but appears before Β§7 and Β§8
178
+ - Cell-11 is **Β§7 Explainability**
179
+ - Cell-13 is **Β§8 Interactive Interface**
180
+
181
+ This makes the notebook hard to follow and looks unpolished for submission.
182
+
183
+ **Steps to fix:**
184
+
185
+ 1. Open the notebook in Colab.
186
+ 2. Rearrange cells so the section flow is:
187
+ - Β§1 Setup & Dependencies
188
+ - Β§2 Synthetic Dataset Generation *(keep for reproducibility reference, but mark as "optional / replaced by real data")*
189
+ - Β§3 ELA Utility
190
+ - Β§4 Data Pipeline (CASIAParser, split_dataset, preload_images, make_dataset)
191
+ - Β§5 Model Architecture (get_rgb_branch, get_ela_branch, build_model)
192
+ - Β§6 Training Engine (`run_training` β€” now added from Fix 1)
193
+ - Β§7 Explainability (get_gradcam)
194
+ - Β§8 Interactive Interface (Gradio demo)
195
+ - Β§9 Execute: 3-Way Ablation Study (the main run cell)
196
+ - Β§10 Results & Evaluation *(new β€” see Fix 4)*
197
+
198
+ 3. Renumber all section headings to match the above order.
199
+ 4. Run all cells again to confirm execution order is correct.
200
+
201
+ ---
202
+
203
+ ### Fix 4 β€” Add Proper Evaluation Metrics
204
+
205
+ **Problem:** The only evaluation metric reported is `accuracy`. For a forensics/detection task on an imbalanced dataset, accuracy alone is misleading β€” a model that always predicts "authentic" would achieve ~71% accuracy on CASIA v2 while being completely useless.
206
+
207
+ **Steps to fix:**
208
+
209
+ 1. After the training/evaluation cell (Β§9), add a new section **Β§10 Results & Evaluation**.
210
+ 2. Add the following evaluation code:
211
+
212
+ ```python
213
+ from sklearn.metrics import (
214
+ confusion_matrix, classification_report,
215
+ roc_auc_score, RocCurveDisplay
216
+ )
217
+ import matplotlib.pyplot as plt
218
+ import seaborn as sns
219
+
220
+ # --- Get predictions on the test set ---
221
+ test_ds_m3 = adapt_dataset_for_model(
222
+ make_dataset(test_rgb, test_ela, test_labels, repeat=False), 'M3'
223
+ )
224
+
225
+ y_pred_prob = model_m3.predict(test_ds_m3, verbose=0).flatten()
226
+ y_pred = (y_pred_prob > 0.5).astype(int)
227
+ y_true = test_labels
228
+
229
+ # --- Classification Report ---
230
+ print("="*50)
231
+ print("M3 (Fused) β€” Classification Report")
232
+ print("="*50)
233
+ print(classification_report(y_true, y_pred, target_names=['Authentic', 'Forged']))
234
+
235
+ # --- Confusion Matrix ---
236
+ cm = confusion_matrix(y_true, y_pred)
237
+ fig, ax = plt.subplots(figsize=(5, 4))
238
+ sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
239
+ xticklabels=['Authentic', 'Forged'],
240
+ yticklabels=['Authentic', 'Forged'])
241
+ ax.set_xlabel('Predicted')
242
+ ax.set_ylabel('Actual')
243
+ ax.set_title('M3 Confusion Matrix')
244
+ plt.tight_layout()
245
+ plt.savefig('confusion_matrix_m3.png', dpi=150)
246
+ plt.show()
247
+
248
+ # --- ROC-AUC ---
249
+ auc = roc_auc_score(y_true, y_pred_prob)
250
+ print(f"ROC-AUC Score: {auc:.4f}")
251
+ RocCurveDisplay.from_predictions(y_true, y_pred_prob)
252
+ plt.title("M3 ROC Curve")
253
+ plt.savefig('roc_curve_m3.png', dpi=150)
254
+ plt.show()
255
+ ```
256
+
257
+ 3. Also run the same evaluation for M1 and M2, then produce a **comparison table**:
258
+
259
+ ```python
260
+ results = {}
261
+ for name, model, m_type in [("M1_RGB", model_m1, 'M1'),
262
+ ("M2_ELA", model_m2, 'M2'),
263
+ ("M3_Fused", model_m3, 'M3')]:
264
+ ds = adapt_dataset_for_model(
265
+ make_dataset(test_rgb, test_ela, test_labels, repeat=False), m_type
266
+ )
267
+ probs = model.predict(ds, verbose=0).flatten()
268
+ preds = (probs > 0.5).astype(int)
269
+ from sklearn.metrics import f1_score, precision_score, recall_score
270
+ results[name] = {
271
+ 'Accuracy': np.mean(preds == test_labels),
272
+ 'Precision': precision_score(test_labels, preds, zero_division=0),
273
+ 'Recall': recall_score(test_labels, preds, zero_division=0),
274
+ 'F1': f1_score(test_labels, preds, zero_division=0),
275
+ 'AUC': roc_auc_score(test_labels, probs),
276
+ }
277
+
278
+ import pandas as pd
279
+ df_results = pd.DataFrame(results).T
280
+ print("\nAblation Study Results")
281
+ print(df_results.to_string(float_format="{:.4f}".format))
282
+ ```
283
+
284
+ 4. Save `confusion_matrix_m3.png` and `roc_curve_m3.png` β€” include these images in your project report.
285
+
286
+ ---
287
+
288
+ ### Fix 5 β€” Make the Ablation Study Meaningful
289
+
290
+ **Problem:** With synthetic data, M1=M2=M3=100% β€” the ablation study proves nothing. With real CASIA v2 data (from Fix 2), the three models will produce genuinely different results, making the ablation meaningful.
291
+
292
+ **Steps to fix (depends on Fix 2 and Fix 4 being done first):**
293
+
294
+ 1. Once real data training is complete, you should see a pattern similar to published results:
295
+ - M1 (RGB only): moderate accuracy, ~75–85%
296
+ - M2 (ELA only): lower accuracy on complex forgeries, ~70–80%
297
+ - M3 (Fused): highest accuracy, ~85–92%
298
+ 2. The comparison table from Fix 4 is your ablation study table.
299
+ 3. In the notebook, add a markdown cell before the results table with this text:
300
+
301
+ ```markdown
302
+ ## Ablation Study: Why Fusion Works
303
+
304
+ | Model | Input | Expected Strength | Expected Weakness |
305
+ |-------|-------|-------------------|-------------------|
306
+ | M1 (RGB) | Original image | Detects semantic inconsistencies | Misses compression artifacts |
307
+ | M2 (ELA) | ELA residuals | Detects compression tampering | Misses structural forgeries |
308
+ | M3 (Fused) | Both | Combines both signals | Slightly slower inference |
309
+
310
+ The fused model (M3) is expected to outperform both single-branch models because it
311
+ combines semantic visual evidence (RGB) with forensic compression evidence (ELA).
312
+ ```
313
+
314
+ ---
315
+
316
+ ## MEDIUM PRIORITY FIXES
317
+
318
+ ---
319
+
320
+ ### Fix 6 β€” Update `train.py` to Match the Notebook
321
+
322
+ **Problem:** `train.py` only builds and trains M3. It is missing M1, M2, the `adapt_dataset_for_model()` helper, and the `run_training()` function β€” all of which exist in the notebook.
323
+
324
+ **Steps to fix:**
325
+
326
+ 1. Add `adapt_dataset_for_model()` from the notebook to `train.py`.
327
+ 2. Add `run_training()` (same function from Fix 1) to `train.py`.
328
+ 3. Update the `build_model()` function signature to accept a `model_type` parameter (`'M1'`, `'M2'`, `'M3'`) β€” same as the notebook version.
329
+ 4. Update the `if __name__ == "__main__":` block to train all three models and print the ablation comparison.
330
+
331
+ ---
332
+
333
+ ### Fix 7 β€” Add the Project Report to the Repository
334
+
335
+ **Problem:** `README.md` references `Documents/Project_Report_Digital_Image_Forgery_Detector.docx` but neither the file nor the `Documents/` folder exists in the repo.
336
+
337
+ **Steps to fix:**
338
+
339
+ 1. Create the `Documents/` folder in the repo root.
340
+ 2. Place the project report `.docx` file inside it.
341
+ 3. `git add Documents/` and commit.
342
+
343
+ If the report does not yet exist, remove the reference from `README.md` until it is ready.
344
+
345
+ ---
346
+
347
+ ### Fix 8 β€” Add a Literature Comparison Section
348
+
349
+ **Problem:** The notebook does not reference any published results on CASIA v2, making it impossible for an examiner to judge whether the results are competitive.
350
+
351
+ **Steps to fix:**
352
+
353
+ 1. After the results table (Β§10), add a markdown cell titled **"Comparison with Published Baselines"**.
354
+ 2. Include a table similar to this (fill in your actual results after Fix 2):
355
+
356
+ ```markdown
357
+ ## Comparison with Published Baselines (CASIA v2)
358
+
359
+ | Method | Accuracy | F1 | Notes |
360
+ |--------|----------|----|-------|
361
+ | Rao et al. (2016) β€” CNN on SRM features | 82.2% | β€” | Single-branch |
362
+ | Salloum et al. (2018) β€” FCN | 89.3% | β€” | Pixel-level |
363
+ | **Our M1 (RGB only)** | _your result_ | _your result_ | ResNet50 |
364
+ | **Our M2 (ELA only)** | _your result_ | _your result_ | Custom CNN |
365
+ | **Our M3 (Fused)** | _your result_ | _your result_ | Dual-branch |
366
+
367
+ Note: Published results use full CASIA v2; our results use the same dataset with
368
+ an 80/10/10 train/val/test split.
369
+ ```
370
+
371
+ 3. Add a brief (2–3 sentence) comment in the markdown on whether your M3 result is competitive and why it may be higher or lower.
372
+
373
+ ---
374
+
375
+ ## LOW PRIORITY FIXES
376
+
377
+ ---
378
+
379
+ ### Fix 9 β€” Align `get_gradcam()` in `app.py` with the Notebook
380
+
381
+ **Problem:** The notebook's `get_gradcam()` uses a `model_type` parameter to intelligently pick the correct last conv layer for each model variant. The `app.py` version is a simplified copy that only searches for `conv2d` named layers.
382
+
383
+ This currently works correctly for M3 since the last `conv2d` in M3 belongs to the ELA branch, which is the forensically meaningful branch. However, if the model is ever updated or swapped, this could silently pick the wrong layer.
384
+
385
+ **Steps to fix:**
386
+
387
+ 1. In `app.py`, replace the `get_gradcam()` function with the more robust version from the notebook.
388
+ 2. Since `app.py` only ever runs M3, hard-code `model_type='M3'` in the call:
389
+ ```python
390
+ heatmap = get_gradcam(m3, input_data, model_type='M3')
391
+ ```
392
+
393
+ ---
394
+
395
+ ### Fix 10 β€” Document the Gradio β†’ Streamlit Difference
396
+
397
+ **Problem:** The notebook uses **Gradio** for its interactive demo (Colab-native), while the deployed app uses **Streamlit** (Hugging Face Spaces). This difference is intentional and correct, but it is not explained anywhere, which could confuse an examiner.
398
+
399
+ **Steps to fix:**
400
+
401
+ 1. Add a sentence to `README.md` under a new heading **"Development vs Deployment UI"**:
402
+
403
+ ```markdown
404
+ ## Development vs Deployment UI
405
+
406
+ The Colab notebook uses **Gradio** for its interactive demo because Gradio works
407
+ natively within Colab with a public share link. The deployed Hugging Face Space
408
+ uses **Streamlit** because it is the SDK configured in the Space settings.
409
+ Both interfaces implement identical inference logic.
410
+ ```
411
+
412
+ ---
413
+
414
+ ## Implementation Order (Recommended)
415
+
416
+ Work through the fixes in this order to avoid rework:
417
+
418
+ ```
419
+ Fix 2a-b β†’ Download and mount CASIA v2 data
420
+ Fix 1 β†’ Add run_training() to notebook
421
+ Fix 3 β†’ Reorder notebook sections
422
+ Fix 2c-e β†’ Retrain on real data, save new M3_best.keras
423
+ Fix 4 β†’ Add confusion matrix, F1, ROC-AUC
424
+ Fix 5 β†’ Validate and write up ablation study
425
+ Fix 6 β†’ Update train.py to match notebook
426
+ Fix 7 β†’ Add project report to repo
427
+ Fix 8 β†’ Add literature comparison
428
+ Fix 9 β†’ Improve get_gradcam in app.py
429
+ Fix 10 β†’ Update README.md
430
+ ```
431
+
432
+ ---
433
+
434
+ ## Definition of Done
435
+
436
+ The submission is ready when:
437
+
438
+ - [ ] Notebook runs end-to-end in Colab without errors (no missing functions)
439
+ - [ ] Notebook sections are numbered and ordered correctly
440
+ - [ ] Training uses real CASIA v2 data (not synthetic noise)
441
+ - [ ] Accuracy is in a realistic range (75–92%) β€” not 100%
442
+ - [ ] Class imbalance is handled via class weights
443
+ - [ ] Evaluation section includes: confusion matrix, precision, recall, F1, ROC-AUC
444
+ - [ ] Ablation study table compares M1, M2, M3 across all metrics
445
+ - [ ] Literature comparison table is present with at least two baselines
446
+ - [ ] `train.py` trains all three models (M1, M2, M3)
447
+ - [ ] `M3_best.keras` in the repo was trained on real data
448
+ - [ ] `Documents/` folder contains the project report
449
+ - [ ] `README.md` explains the Gradio vs Streamlit difference
450
+ - [ ] HF Space is redeployed with the new model weights
documents/remediation_plan.html ADDED
@@ -0,0 +1,1084 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Remediation & Enhancement Plan β€” Image Forgery Detector</title>
7
+ <style>
8
+ /* ── Reset & Base ─────────────────────────────────────────────── */
9
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
10
+
11
+ :root {
12
+ --critical: #dc2626;
13
+ --critical-bg: #fef2f2;
14
+ --critical-border: #fca5a5;
15
+ --high: #d97706;
16
+ --high-bg: #fffbeb;
17
+ --high-border: #fcd34d;
18
+ --medium: #2563eb;
19
+ --medium-bg: #eff6ff;
20
+ --medium-border: #93c5fd;
21
+ --low: #16a34a;
22
+ --low-bg: #f0fdf4;
23
+ --low-border: #86efac;
24
+ --code-bg: #0f172a;
25
+ --code-text: #e2e8f0;
26
+ --sidebar-w: 270px;
27
+ --header-h: 64px;
28
+ }
29
+
30
+ body {
31
+ font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
32
+ font-size: 15px;
33
+ line-height: 1.7;
34
+ color: #1e293b;
35
+ background: #f8fafc;
36
+ }
37
+
38
+ /* ── Top Header ───────────────────────────────────────────────── */
39
+ .top-header {
40
+ position: fixed; top: 0; left: 0; right: 0;
41
+ height: var(--header-h);
42
+ background: #0f172a;
43
+ display: flex; align-items: center; padding: 0 32px;
44
+ z-index: 200;
45
+ gap: 16px;
46
+ }
47
+ .top-header .shield { font-size: 26px; }
48
+ .top-header h1 {
49
+ font-size: 17px; font-weight: 700; color: #f1f5f9;
50
+ line-height: 1.2;
51
+ }
52
+ .top-header h1 span { color: #94a3b8; font-weight: 400; font-size: 13px; display: block; }
53
+ .top-header .tag {
54
+ margin-left: auto;
55
+ background: #1e3a5f;
56
+ color: #93c5fd;
57
+ font-size: 12px; font-weight: 600;
58
+ padding: 4px 12px; border-radius: 20px;
59
+ border: 1px solid #2563eb44;
60
+ }
61
+
62
+ /* ── Sidebar ──────────────────────────────────────────────────── */
63
+ .sidebar {
64
+ position: fixed;
65
+ top: var(--header-h); left: 0; bottom: 0;
66
+ width: var(--sidebar-w);
67
+ background: #fff;
68
+ border-right: 1px solid #e2e8f0;
69
+ overflow-y: auto;
70
+ padding: 24px 0;
71
+ z-index: 100;
72
+ }
73
+ .sidebar-section {
74
+ padding: 6px 24px 2px;
75
+ font-size: 10px; font-weight: 700;
76
+ letter-spacing: 0.1em; text-transform: uppercase;
77
+ color: #94a3b8;
78
+ margin-top: 12px;
79
+ }
80
+ .sidebar a {
81
+ display: flex; align-items: center; gap: 10px;
82
+ padding: 7px 24px;
83
+ font-size: 13px; color: #475569;
84
+ text-decoration: none;
85
+ border-left: 3px solid transparent;
86
+ transition: all 0.15s;
87
+ }
88
+ .sidebar a:hover {
89
+ background: #f1f5f9;
90
+ color: #0f172a;
91
+ border-left-color: #94a3b8;
92
+ }
93
+ .sidebar a .badge {
94
+ font-size: 10px; font-weight: 700;
95
+ padding: 2px 6px; border-radius: 4px;
96
+ flex-shrink: 0;
97
+ }
98
+ .badge.critical { background: var(--critical-bg); color: var(--critical); }
99
+ .badge.high { background: var(--high-bg); color: var(--high); }
100
+ .badge.medium { background: var(--medium-bg); color: var(--medium); }
101
+ .badge.low { background: var(--low-bg); color: var(--low); }
102
+
103
+ /* ── Main Content ─────────────────────────────────────────────── */
104
+ .main {
105
+ margin-left: var(--sidebar-w);
106
+ margin-top: var(--header-h);
107
+ padding: 40px 48px 80px;
108
+ max-width: 960px;
109
+ }
110
+
111
+ /* ── Hero ─────────────────────────────────────────────────────── */
112
+ .hero {
113
+ background: linear-gradient(135deg, #0f172a 0%, #1e3a5f 100%);
114
+ border-radius: 16px;
115
+ padding: 40px 44px;
116
+ margin-bottom: 40px;
117
+ color: #f1f5f9;
118
+ }
119
+ .hero h2 { font-size: 26px; font-weight: 800; margin-bottom: 8px; }
120
+ .hero p { color: #94a3b8; font-size: 14px; max-width: 600px; }
121
+ .hero .stats {
122
+ display: flex; gap: 24px; margin-top: 28px; flex-wrap: wrap;
123
+ }
124
+ .hero .stat {
125
+ background: rgba(255,255,255,0.06);
126
+ border: 1px solid rgba(255,255,255,0.1);
127
+ border-radius: 10px;
128
+ padding: 14px 20px;
129
+ min-width: 110px;
130
+ }
131
+ .hero .stat .n { font-size: 28px; font-weight: 800; line-height: 1; }
132
+ .hero .stat .l { font-size: 11px; color: #94a3b8; margin-top: 4px; }
133
+ .hero .stat.c .n { color: #f87171; }
134
+ .hero .stat.h .n { color: #fbbf24; }
135
+ .hero .stat.m .n { color: #60a5fa; }
136
+ .hero .stat.lo .n { color: #4ade80; }
137
+
138
+ /* ── Issue Summary Table ──────────────────────────────────────── */
139
+ .summary-table {
140
+ width: 100%; border-collapse: collapse;
141
+ background: #fff; border-radius: 12px;
142
+ overflow: hidden;
143
+ box-shadow: 0 1px 3px rgba(0,0,0,0.08);
144
+ margin-bottom: 48px;
145
+ }
146
+ .summary-table th {
147
+ background: #f1f5f9;
148
+ text-align: left; padding: 12px 16px;
149
+ font-size: 12px; font-weight: 700;
150
+ letter-spacing: 0.05em; text-transform: uppercase;
151
+ color: #64748b;
152
+ }
153
+ .summary-table td {
154
+ padding: 12px 16px; font-size: 13.5px;
155
+ border-top: 1px solid #f1f5f9;
156
+ vertical-align: middle;
157
+ }
158
+ .summary-table tr:hover td { background: #fafafa; }
159
+ .summary-table td code {
160
+ background: #f1f5f9; padding: 2px 6px;
161
+ border-radius: 4px; font-size: 12px;
162
+ font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
163
+ color: #0f172a;
164
+ }
165
+ .num-badge {
166
+ display: inline-flex; align-items: center; justify-content: center;
167
+ width: 24px; height: 24px; border-radius: 50%;
168
+ font-size: 12px; font-weight: 700;
169
+ background: #e2e8f0; color: #475569;
170
+ }
171
+
172
+ /* ── Section Headings ─────────────────────────────────────────── */
173
+ .section-header {
174
+ display: flex; align-items: center; gap: 12px;
175
+ margin: 48px 0 24px;
176
+ }
177
+ .section-header .pill {
178
+ padding: 6px 18px; border-radius: 99px;
179
+ font-size: 12px; font-weight: 800;
180
+ letter-spacing: 0.08em; text-transform: uppercase;
181
+ }
182
+ .pill.critical { background: var(--critical); color: #fff; }
183
+ .pill.high { background: var(--high); color: #fff; }
184
+ .pill.medium { background: var(--medium); color: #fff; }
185
+ .pill.low { background: var(--low); color: #fff; }
186
+ .section-header hr {
187
+ flex: 1; border: none; border-top: 2px solid #e2e8f0;
188
+ }
189
+
190
+ /* ── Fix Cards ────────────────────────────────────────────────── */
191
+ .fix-card {
192
+ background: #fff;
193
+ border-radius: 14px;
194
+ border: 1.5px solid #e2e8f0;
195
+ margin-bottom: 28px;
196
+ overflow: hidden;
197
+ box-shadow: 0 1px 3px rgba(0,0,0,0.06);
198
+ }
199
+ .fix-card.critical { border-color: var(--critical-border); }
200
+ .fix-card.high { border-color: var(--high-border); }
201
+ .fix-card.medium { border-color: var(--medium-border); }
202
+ .fix-card.low { border-color: var(--low-border); }
203
+
204
+ .fix-header {
205
+ display: flex; align-items: center; gap: 14px;
206
+ padding: 20px 24px;
207
+ border-bottom: 1px solid #f1f5f9;
208
+ }
209
+ .fix-num {
210
+ width: 36px; height: 36px; border-radius: 50%;
211
+ display: flex; align-items: center; justify-content: center;
212
+ font-size: 15px; font-weight: 800; flex-shrink: 0;
213
+ }
214
+ .fix-card.critical .fix-num { background: var(--critical-bg); color: var(--critical); }
215
+ .fix-card.high .fix-num { background: var(--high-bg); color: var(--high); }
216
+ .fix-card.medium .fix-num { background: var(--medium-bg); color: var(--medium); }
217
+ .fix-card.low .fix-num { background: var(--low-bg); color: var(--low); }
218
+
219
+ .fix-title { font-size: 16px; font-weight: 700; color: #0f172a; }
220
+ .fix-body { padding: 24px; }
221
+
222
+ .problem-box {
223
+ border-radius: 10px;
224
+ padding: 16px 20px;
225
+ margin-bottom: 20px;
226
+ font-size: 14px;
227
+ }
228
+ .fix-card.critical .problem-box { background: var(--critical-bg); border-left: 4px solid var(--critical); }
229
+ .fix-card.high .problem-box { background: var(--high-bg); border-left: 4px solid var(--high); }
230
+ .fix-card.medium .problem-box { background: var(--medium-bg); border-left: 4px solid var(--medium); }
231
+ .fix-card.low .problem-box { background: var(--low-bg); border-left: 4px solid var(--low); }
232
+
233
+ .problem-box .label {
234
+ font-size: 11px; font-weight: 700; text-transform: uppercase;
235
+ letter-spacing: 0.08em; margin-bottom: 6px;
236
+ }
237
+ .fix-card.critical .problem-box .label { color: var(--critical); }
238
+ .fix-card.high .problem-box .label { color: var(--high); }
239
+ .fix-card.medium .problem-box .label { color: var(--medium); }
240
+ .fix-card.low .problem-box .label { color: var(--low); }
241
+
242
+ .why-box {
243
+ background: #f8fafc; border-radius: 8px;
244
+ padding: 12px 16px; margin-bottom: 20px;
245
+ font-size: 13.5px; color: #475569;
246
+ border: 1px solid #e2e8f0;
247
+ }
248
+ .why-box strong { color: #1e293b; }
249
+
250
+ /* ── Steps ────────────────────────────────────────────────────── */
251
+ .steps { counter-reset: step; }
252
+ .step {
253
+ display: flex; gap: 14px;
254
+ margin-bottom: 14px; align-items: flex-start;
255
+ }
256
+ .step::before {
257
+ counter-increment: step;
258
+ content: counter(step);
259
+ min-width: 26px; height: 26px;
260
+ background: #1e293b; color: #fff;
261
+ border-radius: 50%;
262
+ display: flex; align-items: center; justify-content: center;
263
+ font-size: 12px; font-weight: 700; flex-shrink: 0;
264
+ margin-top: 2px;
265
+ }
266
+ .step p { font-size: 14px; color: #374151; }
267
+
268
+ /* ── Sub-steps ────────────────────────────────────────────────── */
269
+ .substep-heading {
270
+ font-size: 13px; font-weight: 700;
271
+ color: #0f172a; margin: 20px 0 10px;
272
+ display: flex; align-items: center; gap: 8px;
273
+ }
274
+ .substep-heading::before {
275
+ content: ''; display: block;
276
+ width: 3px; height: 14px;
277
+ background: #cbd5e1; border-radius: 2px;
278
+ }
279
+
280
+ /* ── Code Blocks ──────────────────────────────────────────────── */
281
+ .code-wrap {
282
+ background: var(--code-bg);
283
+ border-radius: 10px;
284
+ margin: 14px 0;
285
+ overflow: hidden;
286
+ }
287
+ .code-label {
288
+ background: #1e293b;
289
+ padding: 7px 16px;
290
+ font-size: 11px; font-weight: 600;
291
+ color: #94a3b8; letter-spacing: 0.05em;
292
+ display: flex; align-items: center; gap: 8px;
293
+ }
294
+ .code-label::before {
295
+ content: '';
296
+ display: inline-block; width: 8px; height: 8px;
297
+ background: #4ade80; border-radius: 50%;
298
+ }
299
+ pre {
300
+ padding: 18px 20px;
301
+ overflow-x: auto;
302
+ font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
303
+ font-size: 13px;
304
+ line-height: 1.65;
305
+ color: var(--code-text);
306
+ }
307
+ /* Simple syntax colouring via CSS */
308
+ .kw { color: #c084fc; } /* keywords */
309
+ .fn { color: #60a5fa; } /* functions */
310
+ .st { color: #86efac; } /* strings */
311
+ .cm { color: #475569; font-style: italic; } /* comments */
312
+ .nb { color: #fbbf24; } /* numbers */
313
+
314
+ /* ── Expected output box ──────────────────────────────────────── */
315
+ .expected {
316
+ background: #0f2810;
317
+ border: 1px solid #166534;
318
+ border-radius: 8px;
319
+ padding: 12px 16px;
320
+ font-family: 'Cascadia Code', 'Consolas', monospace;
321
+ font-size: 12.5px; color: #86efac;
322
+ margin: 10px 0;
323
+ }
324
+ .expected .exp-label {
325
+ font-size: 10px; font-weight: 700;
326
+ text-transform: uppercase; letter-spacing: 0.08em;
327
+ color: #4ade80; margin-bottom: 6px;
328
+ font-family: 'Segoe UI', sans-serif;
329
+ }
330
+
331
+ /* ── Inline code ──────────────────────────────────────────────── */
332
+ code {
333
+ background: #f1f5f9; padding: 2px 6px;
334
+ border-radius: 4px; font-size: 12.5px;
335
+ font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
336
+ color: #0f172a;
337
+ }
338
+
339
+ /* ── Info callout ─────────────────────────────────────────────── */
340
+ .callout {
341
+ background: #eff6ff; border-left: 4px solid #3b82f6;
342
+ border-radius: 0 8px 8px 0;
343
+ padding: 12px 16px; margin: 14px 0;
344
+ font-size: 13.5px; color: #1e40af;
345
+ }
346
+
347
+ /* ── Ablation table ───────────────────────────────────────────── */
348
+ .model-table {
349
+ width: 100%; border-collapse: collapse;
350
+ font-size: 13px; margin: 14px 0;
351
+ }
352
+ .model-table th {
353
+ background: #1e293b; color: #e2e8f0;
354
+ padding: 10px 14px; text-align: left;
355
+ font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
356
+ }
357
+ .model-table td {
358
+ padding: 10px 14px;
359
+ border-bottom: 1px solid #f1f5f9;
360
+ }
361
+ .model-table tr:nth-child(even) td { background: #f8fafc; }
362
+
363
+ /* ── Implementation order ─────────────────────────────────────── */
364
+ .order-flow {
365
+ display: flex; flex-wrap: wrap; gap: 10px;
366
+ align-items: center; margin: 16px 0;
367
+ }
368
+ .order-step {
369
+ background: #1e293b; color: #e2e8f0;
370
+ padding: 8px 14px; border-radius: 8px;
371
+ font-size: 13px; font-weight: 600;
372
+ }
373
+ .order-step span { color: #94a3b8; font-weight: 400; font-size: 11px; display: block; }
374
+ .order-arrow { color: #94a3b8; font-size: 18px; }
375
+
376
+ /* ── Checklist ────────────────────────────────────────────────── */
377
+ .checklist {
378
+ list-style: none;
379
+ background: #fff;
380
+ border: 1.5px solid #e2e8f0;
381
+ border-radius: 14px;
382
+ overflow: hidden;
383
+ box-shadow: 0 1px 3px rgba(0,0,0,0.06);
384
+ }
385
+ .checklist li {
386
+ display: flex; align-items: flex-start; gap: 12px;
387
+ padding: 13px 20px;
388
+ border-bottom: 1px solid #f1f5f9;
389
+ font-size: 14px;
390
+ cursor: pointer;
391
+ transition: background 0.1s;
392
+ }
393
+ .checklist li:last-child { border-bottom: none; }
394
+ .checklist li:hover { background: #f8fafc; }
395
+ .checklist li input[type=checkbox] {
396
+ margin-top: 3px; width: 16px; height: 16px;
397
+ accent-color: #16a34a; cursor: pointer; flex-shrink: 0;
398
+ }
399
+ .checklist li label { cursor: pointer; }
400
+ .checklist li input:checked + label { text-decoration: line-through; color: #94a3b8; }
401
+
402
+ /* ── Section scroll target padding ───────────────────────────── */
403
+ section[id] { scroll-margin-top: calc(var(--header-h) + 20px); }
404
+
405
+ /* ── Responsive ───────────────────────────────────────────────── */
406
+ @media (max-width: 768px) {
407
+ .sidebar { display: none; }
408
+ .main { margin-left: 0; padding: 24px 20px 60px; }
409
+ }
410
+ </style>
411
+ </head>
412
+ <body>
413
+
414
+ <!-- ── Top Header ───────────────────────────────────────────────── -->
415
+ <header class="top-header">
416
+ <span class="shield">πŸ›‘οΈ</span>
417
+ <h1>Image Forgery Detector
418
+ <span>Remediation &amp; Enhancement Plan β€” PGD Student Project</span>
419
+ </h1>
420
+ <span class="tag">10 Issues Β· 4 Severity Levels</span>
421
+ </header>
422
+
423
+ <!-- ── Sidebar ──────────────────────────────────────────────────── -->
424
+ <nav class="sidebar">
425
+ <div class="sidebar-section">Overview</div>
426
+ <a href="#summary">Issue Summary</a>
427
+ <a href="#order">Implementation Order</a>
428
+ <a href="#done">Definition of Done</a>
429
+
430
+ <div class="sidebar-section">Critical Fixes</div>
431
+ <a href="#fix1"><span class="badge critical">C</span> Fix 1 β€” run_training()</a>
432
+ <a href="#fix2"><span class="badge critical">C</span> Fix 2 β€” Real Dataset</a>
433
+
434
+ <div class="sidebar-section">High Priority</div>
435
+ <a href="#fix3"><span class="badge high">H</span> Fix 3 β€” Notebook Order</a>
436
+ <a href="#fix4"><span class="badge high">H</span> Fix 4 β€” Eval Metrics</a>
437
+ <a href="#fix5"><span class="badge high">H</span> Fix 5 β€” Ablation Study</a>
438
+
439
+ <div class="sidebar-section">Medium Priority</div>
440
+ <a href="#fix6"><span class="badge medium">M</span> Fix 6 β€” train.py Parity</a>
441
+ <a href="#fix7"><span class="badge medium">M</span> Fix 7 β€” Project Report</a>
442
+ <a href="#fix8"><span class="badge medium">M</span> Fix 8 β€” Literature</a>
443
+
444
+ <div class="sidebar-section">Low Priority</div>
445
+ <a href="#fix9"><span class="badge low">L</span> Fix 9 β€” Grad-CAM</a>
446
+ <a href="#fix10"><span class="badge low">L</span> Fix 10 β€” README</a>
447
+ </nav>
448
+
449
+ <!-- ── Main ─────────────────────────────────────────────────────── -->
450
+ <main class="main">
451
+
452
+ <!-- Hero -->
453
+ <div class="hero">
454
+ <h2>Remediation &amp; Enhancement Plan</h2>
455
+ <p>Every gap found during validation, with step-by-step actions to fix each one.
456
+ Work through fixes <strong>in priority order</strong> β€” later fixes depend on earlier ones.</p>
457
+ <div class="stats">
458
+ <div class="stat c"><div class="n">2</div><div class="l">Critical</div></div>
459
+ <div class="stat h"><div class="n">3</div><div class="l">High</div></div>
460
+ <div class="stat m"><div class="n">3</div><div class="l">Medium</div></div>
461
+ <div class="stat lo"><div class="n">2</div><div class="l">Low</div></div>
462
+ </div>
463
+ </div>
464
+
465
+ <!-- Issue Summary -->
466
+ <section id="summary">
467
+ <h2 style="font-size:20px;font-weight:800;margin-bottom:16px;color:#0f172a;">Quick Reference: All Issues</h2>
468
+ <table class="summary-table">
469
+ <thead>
470
+ <tr>
471
+ <th>#</th><th>Severity</th><th>Issue</th><th>Files Affected</th>
472
+ </tr>
473
+ </thead>
474
+ <tbody>
475
+ <tr>
476
+ <td><span class="num-badge">1</span></td>
477
+ <td><span class="badge critical">CRITICAL</span></td>
478
+ <td><code>run_training()</code> never defined in notebook</td>
479
+ <td><code>.ipynb</code></td>
480
+ </tr>
481
+ <tr>
482
+ <td><span class="num-badge">2</span></td>
483
+ <td><span class="badge critical">CRITICAL</span></td>
484
+ <td>Synthetic toy data β€” model learns nothing real</td>
485
+ <td><code>.ipynb</code>, <code>train.py</code></td>
486
+ </tr>
487
+ <tr>
488
+ <td><span class="num-badge">3</span></td>
489
+ <td><span class="badge high">HIGH</span></td>
490
+ <td>Notebook section order is broken (Β§9 β†’ Β§7 β†’ Β§8)</td>
491
+ <td><code>.ipynb</code></td>
492
+ </tr>
493
+ <tr>
494
+ <td><span class="num-badge">4</span></td>
495
+ <td><span class="badge high">HIGH</span></td>
496
+ <td>No meaningful evaluation metrics (only accuracy)</td>
497
+ <td><code>.ipynb</code></td>
498
+ </tr>
499
+ <tr>
500
+ <td><span class="num-badge">5</span></td>
501
+ <td><span class="badge high">HIGH</span></td>
502
+ <td>Ablation study conclusion is invalid (all 100%)</td>
503
+ <td><code>.ipynb</code></td>
504
+ </tr>
505
+ <tr>
506
+ <td><span class="num-badge">6</span></td>
507
+ <td><span class="badge medium">MEDIUM</span></td>
508
+ <td><code>train.py</code> only trains M3, not M1/M2</td>
509
+ <td><code>train.py</code></td>
510
+ </tr>
511
+ <tr>
512
+ <td><span class="num-badge">7</span></td>
513
+ <td><span class="badge medium">MEDIUM</span></td>
514
+ <td>Project report document missing from repo</td>
515
+ <td><code>Documents/</code></td>
516
+ </tr>
517
+ <tr>
518
+ <td><span class="num-badge">8</span></td>
519
+ <td><span class="badge medium">MEDIUM</span></td>
520
+ <td>No literature comparison or baseline results</td>
521
+ <td><code>.ipynb</code></td>
522
+ </tr>
523
+ <tr>
524
+ <td><span class="num-badge">9</span></td>
525
+ <td><span class="badge low">LOW</span></td>
526
+ <td><code>get_gradcam()</code> in app.py is simplified vs notebook</td>
527
+ <td><code>app.py</code></td>
528
+ </tr>
529
+ <tr>
530
+ <td><span class="num-badge">10</span></td>
531
+ <td><span class="badge low">LOW</span></td>
532
+ <td>Gradio vs Streamlit discrepancy not documented</td>
533
+ <td><code>README.md</code></td>
534
+ </tr>
535
+ </tbody>
536
+ </table>
537
+ </section>
538
+
539
+ <!-- ═══════════════════════════ CRITICAL ═════════════════════════ -->
540
+ <div class="section-header">
541
+ <span class="pill critical">Critical Fixes</span>
542
+ <hr>
543
+ </div>
544
+
545
+ <!-- Fix 1 -->
546
+ <section id="fix1">
547
+ <div class="fix-card critical">
548
+ <div class="fix-header">
549
+ <div class="fix-num">1</div>
550
+ <div class="fix-title">Add the Missing <code>run_training()</code> Function to the Notebook</div>
551
+ </div>
552
+ <div class="fix-body">
553
+ <div class="problem-box">
554
+ <div class="label">Problem</div>
555
+ Cell 16 calls <code>run_training('M1', ...)</code>, <code>run_training('M2', ...)</code>, and <code>run_training('M3', ...)</code>
556
+ but this function is <strong>never defined</strong> in any visible cell. The notebook cannot be run
557
+ end-to-end β€” an examiner will get a <code>NameError</code> immediately.
558
+ </div>
559
+ <div class="why-box">
560
+ <strong>Why it matters:</strong> A Colab notebook is expected to be fully self-contained.
561
+ Every function called must be defined above its call site.
562
+ </div>
563
+
564
+ <div class="steps">
565
+ <div class="step"><p>Open <code>Image_Forgery_Detection_Colab_1.ipynb</code> in Google Colab.</p></div>
566
+ <div class="step"><p>Insert a new code cell <strong>between the <code>build_model()</code> cell and the ablation execution cell</strong> (between current cell-9 and cell-16).</p></div>
567
+ <div class="step"><p>Paste the following function into that new cell:</p></div>
568
+ </div>
569
+
570
+ <div class="code-wrap">
571
+ <div class="code-label">Python β€” run_training() definition</div>
572
+ <pre><span class="kw">def</span> <span class="fn">run_training</span>(model_type, train_ds_base, val_ds_base, n_train, n_val):
573
+ <span class="fn">print</span>(<span class="st">f"\n{'='*55}"</span>)
574
+ <span class="fn">print</span>(<span class="st">f"Training {model_type}"</span>)
575
+ <span class="fn">print</span>(<span class="st">f"{'='*55}"</span>)
576
+
577
+ train_ds = <span class="fn">adapt_dataset_for_model</span>(train_ds_base, model_type)
578
+ val_ds = <span class="fn">adapt_dataset_for_model</span>(val_ds_base, model_type)
579
+
580
+ model = <span class="fn">build_model</span>(model_type)
581
+ model.<span class="fn">compile</span>(
582
+ optimizer=<span class="st">'adam'</span>,
583
+ loss=<span class="st">'binary_crossentropy'</span>,
584
+ metrics=[<span class="st">'accuracy'</span>]
585
+ )
586
+
587
+ steps_per_epoch = <span class="fn">max</span>(<span class="nb">1</span>, <span class="fn">int</span>(np.<span class="fn">ceil</span>(n_train / BATCH_SIZE)))
588
+ validation_steps = <span class="fn">max</span>(<span class="nb">1</span>, <span class="fn">int</span>(np.<span class="fn">ceil</span>(n_val / BATCH_SIZE)))
589
+
590
+ history = model.<span class="fn">fit</span>(
591
+ train_ds,
592
+ validation_data=val_ds,
593
+ epochs=EPOCHS,
594
+ steps_per_epoch=steps_per_epoch,
595
+ validation_steps=validation_steps,
596
+ verbose=<span class="nb">1</span>,
597
+ )
598
+
599
+ save_path = <span class="st">f"{model_type}_best.keras"</span>
600
+ model.<span class="fn">save</span>(save_path)
601
+ <span class="fn">print</span>(<span class="st">f"βœ” {model_type} saved β†’ {save_path}"</span>)
602
+ <span class="kw">return</span> model, history</pre>
603
+ </div>
604
+
605
+ <div class="steps">
606
+ <div class="step"><p>Run all cells from top to bottom to confirm there are no errors.</p></div>
607
+ <div class="step"><p>Confirm the output shows three training runs (M1, M2, M3) completing with no <code>NameError</code>.</p></div>
608
+ </div>
609
+ </div>
610
+ </div>
611
+ </section>
612
+
613
+ <!-- Fix 2 -->
614
+ <section id="fix2">
615
+ <div class="fix-card critical">
616
+ <div class="fix-header">
617
+ <div class="fix-num">2</div>
618
+ <div class="fix-title">Replace Synthetic Toy Data with Real CASIA v2</div>
619
+ </div>
620
+ <div class="fix-body">
621
+ <div class="problem-box">
622
+ <div class="label">Problem</div>
623
+ Current dataset: <strong>Authentic</strong> = random noise (RGB 100–200) |
624
+ <strong>Forged</strong> = same noise + a solid red rectangle at [50–150, 50–150].
625
+ The model learns "red rectangle = forged" and gets 100% accuracy β€” this has
626
+ <strong>no relationship to real image forgery detection</strong>.
627
+ </div>
628
+ <div class="why-box">
629
+ <strong>Why it matters:</strong> An examiner will immediately recognise that 100% accuracy on
630
+ 120 synthetic noise images is not a valid result. It is the single biggest weakness in the submission.
631
+ </div>
632
+
633
+ <div class="substep-heading">Step 2a β€” Download CASIA v2</div>
634
+ <div class="steps">
635
+ <div class="step"><p>Go to <strong>Kaggle</strong> and search for <em>"CASIA v2 image forgery"</em> or <em>"CASIA 2.0 dataset"</em>.</p></div>
636
+ <div class="step"><p>Download the dataset (~3.3 GB). It contains ~12,614 authentic images (<code>Au_*</code>) and ~5,123 tampered images (<code>Tp_*</code>).</p></div>
637
+ <div class="step"><p>Upload the extracted folder to your <strong>Google Drive</strong> and name it <code>casia_v2/</code>.</p></div>
638
+ </div>
639
+
640
+ <div class="substep-heading">Step 2b β€” Mount Drive and Update Data Path</div>
641
+ <div class="steps">
642
+ <div class="step"><p>Add this cell at the top of the data section in the notebook:</p></div>
643
+ </div>
644
+ <div class="code-wrap">
645
+ <div class="code-label">Python β€” Mount Google Drive</div>
646
+ <pre><span class="kw">from</span> google.colab <span class="kw">import</span> drive
647
+ drive.<span class="fn">mount</span>(<span class="st">'/content/drive'</span>)
648
+
649
+ TARGET_DIR = <span class="st">"/content/drive/MyDrive/casia_v2"</span> <span class="cm"># adjust if needed</span></pre>
650
+ </div>
651
+ <div class="steps">
652
+ <div class="step"><p>Comment out or remove the call to <code>generate_robust_dataset()</code> β€” synthetic data is no longer needed.</p></div>
653
+ <div class="step"><p>Run <code>split_dataset(TARGET_DIR)</code> directly on the real data path.</p></div>
654
+ </div>
655
+
656
+ <div class="substep-heading">Step 2c β€” Verify the Split</div>
657
+ <div class="code-wrap">
658
+ <div class="code-label">Python β€” Verify split counts and label balance</div>
659
+ <pre>splits = <span class="fn">split_dataset</span>(TARGET_DIR)
660
+ <span class="fn">print</span>(<span class="st">f"Train: {len(splits['train'])} | Val: {len(splits['val'])} | Test: {len(splits['test'])}"</span>)
661
+
662
+ <span class="kw">for</span> split_name, paths <span class="kw">in</span> splits.items():
663
+ authentic = <span class="fn">sum</span>(<span class="nb">1</span> <span class="kw">for</span> p <span class="kw">in</span> paths <span class="kw">if</span> os.path.<span class="fn">basename</span>(p).<span class="fn">startswith</span>(<span class="st">'Au_'</span>))
664
+ forged = <span class="fn">sum</span>(<span class="nb">1</span> <span class="kw">for</span> p <span class="kw">in</span> paths <span class="kw">if</span> os.path.<span class="fn">basename</span>(p).<span class="fn">startswith</span>(<span class="st">'Tp_'</span>))
665
+ <span class="fn">print</span>(<span class="st">f"{split_name}: {authentic} authentic, {forged} forged"</span>)</pre>
666
+ </div>
667
+ <div class="expected">
668
+ <div class="exp-label">Expected Output (approximate)</div>
669
+ Train: ~14000 | Val: ~1700 | Test: ~1700
670
+ train: ~10000 authentic, ~4000 forged
671
+ </div>
672
+
673
+ <div class="substep-heading">Step 2d β€” Handle Class Imbalance</div>
674
+ <div class="callout">
675
+ CASIA v2 has ~2.5Γ— more authentic than tampered images. Without class weights,
676
+ the model will bias toward predicting "authentic" and appear to have high accuracy while missing most forgeries.
677
+ </div>
678
+ <div class="code-wrap">
679
+ <div class="code-label">Python β€” Compute class weights</div>
680
+ <pre><span class="kw">from</span> sklearn.utils.class_weight <span class="kw">import</span> compute_class_weight
681
+
682
+ classes = np.<span class="fn">unique</span>(train_labels)
683
+ weights = <span class="fn">compute_class_weight</span>(<span class="st">'balanced'</span>, classes=classes, y=train_labels)
684
+ class_weight_dict = <span class="fn">dict</span>(<span class="fn">zip</span>(classes, weights))
685
+ <span class="fn">print</span>(<span class="st">"Class weights:"</span>, class_weight_dict)
686
+ <span class="cm"># Then pass class_weight=class_weight_dict to model.fit()</span></pre>
687
+ </div>
688
+
689
+ <div class="substep-heading">Step 2e β€” Retrain and Save</div>
690
+ <div class="steps">
691
+ <div class="step"><p>Run training. Expect accuracy in the range <strong>80–92%</strong> (not 100%). If above 95%, check for data leakage. If below 70%, increase <code>EPOCHS</code> to 10–15.</p></div>
692
+ <div class="step"><p>Download the trained model from Colab:</p></div>
693
+ </div>
694
+ <div class="code-wrap">
695
+ <div class="code-label">Python β€” Download model from Colab</div>
696
+ <pre><span class="kw">from</span> google.colab <span class="kw">import</span> files
697
+ files.<span class="fn">download</span>(<span class="st">'M3_best.keras'</span>)</pre>
698
+ </div>
699
+ <div class="steps">
700
+ <div class="step"><p>Replace the existing <code>M3_best.keras</code> in the repo. Git LFS will handle the large file upload automatically on the next commit.</p></div>
701
+ </div>
702
+ </div>
703
+ </div>
704
+ </section>
705
+
706
+ <!-- ═══════════════════════════ HIGH ═════════════════════════════ -->
707
+ <div class="section-header">
708
+ <span class="pill high">High Priority</span>
709
+ <hr>
710
+ </div>
711
+
712
+ <!-- Fix 3 -->
713
+ <section id="fix3">
714
+ <div class="fix-card high">
715
+ <div class="fix-header">
716
+ <div class="fix-num">3</div>
717
+ <div class="fix-title">Reorder Notebook Sections</div>
718
+ </div>
719
+ <div class="fix-body">
720
+ <div class="problem-box">
721
+ <div class="label">Problem</div>
722
+ Section headings appear in the wrong order: <strong>Β§9</strong> (Execute) appears before
723
+ <strong>Β§7</strong> (Explainability) and <strong>Β§8</strong> (Interface). The notebook is
724
+ hard to follow and looks unpolished for submission.
725
+ </div>
726
+ <div class="steps">
727
+ <div class="step"><p>Open the notebook in Colab and rearrange cells into this order:</p></div>
728
+ </div>
729
+ <table class="model-table" style="margin:14px 0;">
730
+ <thead><tr><th>Section</th><th>Content</th></tr></thead>
731
+ <tbody>
732
+ <tr><td>Β§1</td><td>Setup &amp; Dependencies</td></tr>
733
+ <tr><td>Β§2</td><td>Synthetic Dataset Generation <em>(mark as optional β€” replaced by real data)</em></td></tr>
734
+ <tr><td>Β§3</td><td>ELA Utility (<code>compute_ela</code>)</td></tr>
735
+ <tr><td>Β§4</td><td>Data Pipeline (CASIAParser, split, preload, make_dataset)</td></tr>
736
+ <tr><td>Β§5</td><td>Model Architecture (get_rgb_branch, get_ela_branch, build_model)</td></tr>
737
+ <tr><td>Β§6</td><td>Training Engine (<code>run_training</code> β€” added in Fix 1)</td></tr>
738
+ <tr><td>Β§7</td><td>Explainability (<code>get_gradcam</code>)</td></tr>
739
+ <tr><td>Β§8</td><td>Interactive Interface (Gradio demo)</td></tr>
740
+ <tr><td>Β§9</td><td>Execute: 3-Way Ablation Study</td></tr>
741
+ <tr><td>Β§10</td><td>Results &amp; Evaluation <em>(new β€” see Fix 4)</em></td></tr>
742
+ </tbody>
743
+ </table>
744
+ <div class="steps">
745
+ <div class="step"><p>Renumber all section headings to match the table above.</p></div>
746
+ <div class="step"><p>Run all cells again top-to-bottom to confirm no execution errors.</p></div>
747
+ </div>
748
+ </div>
749
+ </div>
750
+ </section>
751
+
752
+ <!-- Fix 4 -->
753
+ <section id="fix4">
754
+ <div class="fix-card high">
755
+ <div class="fix-header">
756
+ <div class="fix-num">4</div>
757
+ <div class="fix-title">Add Proper Evaluation Metrics</div>
758
+ </div>
759
+ <div class="fix-body">
760
+ <div class="problem-box">
761
+ <div class="label">Problem</div>
762
+ Only <code>accuracy</code> is reported. On an imbalanced dataset like CASIA v2,
763
+ a model that always predicts "authentic" achieves ~71% accuracy while being completely useless.
764
+ Accuracy alone is not sufficient for a forensics task.
765
+ </div>
766
+ <div class="steps">
767
+ <div class="step"><p>After the training cell (Β§9), add a new section <strong>Β§10 Results &amp; Evaluation</strong>.</p></div>
768
+ <div class="step"><p>Add this evaluation code to generate a classification report and confusion matrix:</p></div>
769
+ </div>
770
+ <div class="code-wrap">
771
+ <div class="code-label">Python β€” Classification report + confusion matrix</div>
772
+ <pre><span class="kw">from</span> sklearn.metrics <span class="kw">import</span> (
773
+ confusion_matrix, classification_report,
774
+ roc_auc_score, RocCurveDisplay
775
+ )
776
+ <span class="kw">import</span> matplotlib.pyplot <span class="kw">as</span> plt
777
+ <span class="kw">import</span> seaborn <span class="kw">as</span> sns
778
+
779
+ test_ds_m3 = <span class="fn">adapt_dataset_for_model</span>(
780
+ <span class="fn">make_dataset</span>(test_rgb, test_ela, test_labels, repeat=<span class="kw">False</span>), <span class="st">'M3'</span>
781
+ )
782
+ y_pred_prob = model_m3.<span class="fn">predict</span>(test_ds_m3, verbose=<span class="nb">0</span>).<span class="fn">flatten</span>()
783
+ y_pred = (y_pred_prob > <span class="nb">0.5</span>).astype(<span class="fn">int</span>)
784
+ y_true = test_labels
785
+
786
+ <span class="fn">print</span>(<span class="st">"="*50</span>)
787
+ <span class="fn">print</span>(<span class="st">"M3 (Fused) β€” Classification Report"</span>)
788
+ <span class="fn">print</span>(<span class="st">"="*50</span>)
789
+ <span class="fn">print</span>(<span class="fn">classification_report</span>(y_true, y_pred,
790
+ target_names=[<span class="st">'Authentic'</span>, <span class="st">'Forged'</span>]))
791
+
792
+ cm = <span class="fn">confusion_matrix</span>(y_true, y_pred)
793
+ fig, ax = plt.<span class="fn">subplots</span>(figsize=(<span class="nb">5</span>, <span class="nb">4</span>))
794
+ sns.<span class="fn">heatmap</span>(cm, annot=<span class="kw">True</span>, fmt=<span class="st">'d'</span>, cmap=<span class="st">'Blues'</span>,
795
+ xticklabels=[<span class="st">'Authentic'</span>, <span class="st">'Forged'</span>],
796
+ yticklabels=[<span class="st">'Authentic'</span>, <span class="st">'Forged'</span>])
797
+ ax.<span class="fn">set_xlabel</span>(<span class="st">'Predicted'</span>); ax.<span class="fn">set_ylabel</span>(<span class="st">'Actual'</span>)
798
+ ax.<span class="fn">set_title</span>(<span class="st">'M3 Confusion Matrix'</span>)
799
+ plt.<span class="fn">savefig</span>(<span class="st">'confusion_matrix_m3.png'</span>, dpi=<span class="nb">150</span>)
800
+ plt.<span class="fn">show</span>()
801
+
802
+ auc = <span class="fn">roc_auc_score</span>(y_true, y_pred_prob)
803
+ <span class="fn">print</span>(<span class="st">f"ROC-AUC Score: {auc:.4f}"</span>)
804
+ <span class="fn">RocCurveDisplay</span>.<span class="fn">from_predictions</span>(y_true, y_pred_prob)
805
+ plt.<span class="fn">title</span>(<span class="st">"M3 ROC Curve"</span>)
806
+ plt.<span class="fn">savefig</span>(<span class="st">'roc_curve_m3.png'</span>, dpi=<span class="nb">150</span>)
807
+ plt.<span class="fn">show</span>()</pre>
808
+ </div>
809
+ <div class="steps">
810
+ <div class="step"><p>Add the model comparison table across all three variants:</p></div>
811
+ </div>
812
+ <div class="code-wrap">
813
+ <div class="code-label">Python β€” M1 vs M2 vs M3 metrics table</div>
814
+ <pre>results = {}
815
+ <span class="kw">for</span> name, model, m_type <span class="kw">in</span> [(<span class="st">"M1_RGB"</span>, model_m1, <span class="st">'M1'</span>),
816
+ (<span class="st">"M2_ELA"</span>, model_m2, <span class="st">'M2'</span>),
817
+ (<span class="st">"M3_Fused"</span>, model_m3, <span class="st">'M3'</span>)]:
818
+ ds = <span class="fn">adapt_dataset_for_model</span>(
819
+ <span class="fn">make_dataset</span>(test_rgb, test_ela, test_labels, repeat=<span class="kw">False</span>), m_type)
820
+ probs = model.<span class="fn">predict</span>(ds, verbose=<span class="nb">0</span>).<span class="fn">flatten</span>()
821
+ preds = (probs > <span class="nb">0.5</span>).astype(<span class="fn">int</span>)
822
+ <span class="kw">from</span> sklearn.metrics <span class="kw">import</span> f1_score, precision_score, recall_score
823
+ results[name] = {
824
+ <span class="st">'Accuracy'</span>: np.<span class="fn">mean</span>(preds == test_labels),
825
+ <span class="st">'Precision'</span>: <span class="fn">precision_score</span>(test_labels, preds, zero_division=<span class="nb">0</span>),
826
+ <span class="st">'Recall'</span>: <span class="fn">recall_score</span>(test_labels, preds, zero_division=<span class="nb">0</span>),
827
+ <span class="st">'F1'</span>: <span class="fn">f1_score</span>(test_labels, preds, zero_division=<span class="nb">0</span>),
828
+ <span class="st">'AUC'</span>: <span class="fn">roc_auc_score</span>(test_labels, probs),
829
+ }
830
+
831
+ <span class="kw">import</span> pandas <span class="kw">as</span> pd
832
+ df_results = pd.<span class="fn">DataFrame</span>(results).T
833
+ <span class="fn">print</span>(<span class="st">"\nAblation Study Results"</span>)
834
+ <span class="fn">print</span>(df_results.<span class="fn">to_string</span>(float_format=<span class="st">"{:.4f}"</span>.<span class="fn">format</span>))</pre>
835
+ </div>
836
+ <div class="steps">
837
+ <div class="step"><p>Save <code>confusion_matrix_m3.png</code> and <code>roc_curve_m3.png</code> and include them in the project report.</p></div>
838
+ </div>
839
+ </div>
840
+ </div>
841
+ </section>
842
+
843
+ <!-- Fix 5 -->
844
+ <section id="fix5">
845
+ <div class="fix-card high">
846
+ <div class="fix-header">
847
+ <div class="fix-num">5</div>
848
+ <div class="fix-title">Make the Ablation Study Meaningful</div>
849
+ </div>
850
+ <div class="fix-body">
851
+ <div class="problem-box">
852
+ <div class="label">Problem</div>
853
+ With synthetic data, M1=M2=M3=100% β€” the ablation proves nothing.
854
+ With real CASIA v2, the three models will produce genuinely different results.
855
+ <strong>Depends on Fix 2 and Fix 4 being completed first.</strong>
856
+ </div>
857
+ <div class="steps">
858
+ <div class="step">
859
+ <p>After real-data training, expect results similar to this pattern:</p>
860
+ </div>
861
+ </div>
862
+ <table class="model-table">
863
+ <thead><tr><th>Model</th><th>Input</th><th>Expected Accuracy</th><th>Strength</th><th>Weakness</th></tr></thead>
864
+ <tbody>
865
+ <tr><td>M1 (RGB)</td><td>Original image</td><td>~75–85%</td><td>Semantic inconsistencies</td><td>Misses compression artifacts</td></tr>
866
+ <tr><td>M2 (ELA)</td><td>ELA residuals</td><td>~70–80%</td><td>Compression tampering</td><td>Misses structural forgeries</td></tr>
867
+ <tr><td><strong>M3 (Fused)</strong></td><td>Both</td><td><strong>~85–92%</strong></td><td>Combines both signals</td><td>Slightly slower inference</td></tr>
868
+ </tbody>
869
+ </table>
870
+ <div class="steps">
871
+ <div class="step"><p>The comparison table from Fix 4 is your ablation study table β€” no separate code needed.</p></div>
872
+ <div class="step"><p>Add a markdown cell before the results table explaining why fusion outperforms single-branch models. Use the table above as a guide.</p></div>
873
+ </div>
874
+ </div>
875
+ </div>
876
+ </section>
877
+
878
+ <!-- ═══════════════════════════ MEDIUM ═══════════════════════════ -->
879
+ <div class="section-header">
880
+ <span class="pill medium">Medium Priority</span>
881
+ <hr>
882
+ </div>
883
+
884
+ <!-- Fix 6 -->
885
+ <section id="fix6">
886
+ <div class="fix-card medium">
887
+ <div class="fix-header">
888
+ <div class="fix-num">6</div>
889
+ <div class="fix-title">Update <code>train.py</code> to Match the Notebook</div>
890
+ </div>
891
+ <div class="fix-body">
892
+ <div class="problem-box">
893
+ <div class="label">Problem</div>
894
+ <code>train.py</code> only builds and trains M3. It is missing <code>adapt_dataset_for_model()</code>,
895
+ <code>run_training()</code>, and M1/M2 variants β€” all of which exist in the notebook.
896
+ </div>
897
+ <div class="steps">
898
+ <div class="step"><p>Add <code>adapt_dataset_for_model()</code> from the notebook to <code>train.py</code>.</p></div>
899
+ <div class="step"><p>Add <code>run_training()</code> (same as Fix 1) to <code>train.py</code>.</p></div>
900
+ <div class="step"><p>Update <code>build_model()</code> to accept a <code>model_type</code> parameter (<code>'M1'</code>, <code>'M2'</code>, <code>'M3'</code>) matching the notebook version.</p></div>
901
+ <div class="step"><p>Update the <code>if __name__ == "__main__":</code> block to train all three models and print the ablation comparison table.</p></div>
902
+ </div>
903
+ </div>
904
+ </div>
905
+ </section>
906
+
907
+ <!-- Fix 7 -->
908
+ <section id="fix7">
909
+ <div class="fix-card medium">
910
+ <div class="fix-header">
911
+ <div class="fix-num">7</div>
912
+ <div class="fix-title">Add the Project Report to the Repository</div>
913
+ </div>
914
+ <div class="fix-body">
915
+ <div class="problem-box">
916
+ <div class="label">Problem</div>
917
+ <code>README.md</code> references
918
+ <code>Documents/Project_Report_Digital_Image_Forgery_Detector.docx</code>
919
+ but neither the file nor the <code>Documents/</code> folder exist in the repo.
920
+ </div>
921
+ <div class="steps">
922
+ <div class="step"><p>Create the <code>Documents/</code> folder in the repo root.</p></div>
923
+ <div class="step"><p>Place the project report <code>.docx</code> file inside it.</p></div>
924
+ <div class="step"><p>Run <code>git add Documents/</code> and commit.</p></div>
925
+ </div>
926
+ <div class="callout">
927
+ If the report does not yet exist, remove the reference from <code>README.md</code>
928
+ until it is ready β€” a broken link is worse than no link.
929
+ </div>
930
+ </div>
931
+ </div>
932
+ </section>
933
+
934
+ <!-- Fix 8 -->
935
+ <section id="fix8">
936
+ <div class="fix-card medium">
937
+ <div class="fix-header">
938
+ <div class="fix-num">8</div>
939
+ <div class="fix-title">Add a Literature Comparison Section</div>
940
+ </div>
941
+ <div class="fix-body">
942
+ <div class="problem-box">
943
+ <div class="label">Problem</div>
944
+ The notebook does not reference any published results on CASIA v2,
945
+ making it impossible for an examiner to judge whether the results are competitive.
946
+ </div>
947
+ <div class="steps">
948
+ <div class="step"><p>After the results table (Β§10), add a markdown cell titled <strong>"Comparison with Published Baselines"</strong>.</p></div>
949
+ <div class="step"><p>Use this template (fill in your actual results after Fix 2 is done):</p></div>
950
+ </div>
951
+ <table class="model-table">
952
+ <thead><tr><th>Method</th><th>Accuracy</th><th>F1</th><th>Notes</th></tr></thead>
953
+ <tbody>
954
+ <tr><td>Rao et al. (2016) β€” CNN on SRM features</td><td>82.2%</td><td>β€”</td><td>Single-branch</td></tr>
955
+ <tr><td>Salloum et al. (2018) β€” FCN</td><td>89.3%</td><td>β€”</td><td>Pixel-level</td></tr>
956
+ <tr><td><strong>Our M1 (RGB only)</strong></td><td><em>your result</em></td><td><em>your result</em></td><td>ResNet50</td></tr>
957
+ <tr><td><strong>Our M2 (ELA only)</strong></td><td><em>your result</em></td><td><em>your result</em></td><td>Custom CNN</td></tr>
958
+ <tr><td><strong>Our M3 (Fused)</strong></td><td><em>your result</em></td><td><em>your result</em></td><td>Dual-branch</td></tr>
959
+ </tbody>
960
+ </table>
961
+ <div class="steps">
962
+ <div class="step"><p>Add 2–3 sentences commenting on whether M3 is competitive and why it may be higher or lower than the baselines.</p></div>
963
+ </div>
964
+ </div>
965
+ </div>
966
+ </section>
967
+
968
+ <!-- ═══════════════════════════ LOW ══════════════════════════════ -->
969
+ <div class="section-header">
970
+ <span class="pill low">Low Priority</span>
971
+ <hr>
972
+ </div>
973
+
974
+ <!-- Fix 9 -->
975
+ <section id="fix9">
976
+ <div class="fix-card low">
977
+ <div class="fix-header">
978
+ <div class="fix-num">9</div>
979
+ <div class="fix-title">Align <code>get_gradcam()</code> in <code>app.py</code> with the Notebook</div>
980
+ </div>
981
+ <div class="fix-body">
982
+ <div class="problem-box">
983
+ <div class="label">Problem</div>
984
+ The notebook's <code>get_gradcam()</code> uses a <code>model_type</code> parameter to pick
985
+ the correct last conv layer per model variant. The <code>app.py</code> version only searches
986
+ for <code>conv2d</code> named layers β€” if the model is updated it could silently pick the wrong layer.
987
+ </div>
988
+ <div class="steps">
989
+ <div class="step"><p>In <code>app.py</code>, replace the current <code>get_gradcam()</code> with the more robust version from the notebook.</p></div>
990
+ <div class="step"><p>Since <code>app.py</code> only runs M3, hard-code the call as:</p></div>
991
+ </div>
992
+ <div class="code-wrap">
993
+ <div class="code-label">Python β€” Updated call in app.py</div>
994
+ <pre>heatmap = <span class="fn">get_gradcam</span>(m3, input_data, model_type=<span class="st">'M3'</span>)</pre>
995
+ </div>
996
+ </div>
997
+ </div>
998
+ </section>
999
+
1000
+ <!-- Fix 10 -->
1001
+ <section id="fix10">
1002
+ <div class="fix-card low">
1003
+ <div class="fix-header">
1004
+ <div class="fix-num">10</div>
1005
+ <div class="fix-title">Document the Gradio β†’ Streamlit Difference in README</div>
1006
+ </div>
1007
+ <div class="fix-body">
1008
+ <div class="problem-box">
1009
+ <div class="label">Problem</div>
1010
+ The notebook uses <strong>Gradio</strong> (Colab-native); the deployed app uses
1011
+ <strong>Streamlit</strong> (Hugging Face Spaces). This intentional difference
1012
+ is not explained anywhere and may confuse an examiner.
1013
+ </div>
1014
+ <div class="steps">
1015
+ <div class="step"><p>Add the following section to <code>README.md</code>:</p></div>
1016
+ </div>
1017
+ <div class="code-wrap">
1018
+ <div class="code-label">Markdown β€” README.md addition</div>
1019
+ <pre>## Development vs Deployment UI
1020
+
1021
+ The Colab notebook uses **Gradio** for its interactive demo because Gradio
1022
+ works natively within Colab with a public share link.
1023
+ The deployed Hugging Face Space uses **Streamlit** because it is the SDK
1024
+ configured in the Space settings.
1025
+ Both interfaces implement identical inference logic.</pre>
1026
+ </div>
1027
+ </div>
1028
+ </div>
1029
+ </section>
1030
+
1031
+ <!-- ═════════════════════ Implementation Order ═══════════════════ -->
1032
+ <section id="order" style="margin-top:48px;">
1033
+ <h2 style="font-size:20px;font-weight:800;margin-bottom:16px;color:#0f172a;">Recommended Implementation Order</h2>
1034
+ <p style="color:#64748b;font-size:14px;margin-bottom:20px;">Work through the fixes in this order to avoid rework β€” later fixes depend on earlier ones.</p>
1035
+ <div class="order-flow">
1036
+ <div class="order-step">Fix 2a–b <span>Download CASIA v2</span></div>
1037
+ <span class="order-arrow">β†’</span>
1038
+ <div class="order-step">Fix 1 <span>run_training()</span></div>
1039
+ <span class="order-arrow">β†’</span>
1040
+ <div class="order-step">Fix 3 <span>Reorder notebook</span></div>
1041
+ <span class="order-arrow">β†’</span>
1042
+ <div class="order-step">Fix 2c–e <span>Retrain on real data</span></div>
1043
+ <span class="order-arrow">β†’</span>
1044
+ <div class="order-step">Fix 4 <span>Eval metrics</span></div>
1045
+ <span class="order-arrow">β†’</span>
1046
+ <div class="order-step">Fix 5 <span>Ablation write-up</span></div>
1047
+ <span class="order-arrow">β†’</span>
1048
+ <div class="order-step">Fix 6 <span>train.py parity</span></div>
1049
+ <span class="order-arrow">β†’</span>
1050
+ <div class="order-step">Fix 7 <span>Project report</span></div>
1051
+ <span class="order-arrow">β†’</span>
1052
+ <div class="order-step">Fix 8 <span>Literature</span></div>
1053
+ <span class="order-arrow">β†’</span>
1054
+ <div class="order-step">Fix 9 <span>Grad-CAM</span></div>
1055
+ <span class="order-arrow">β†’</span>
1056
+ <div class="order-step">Fix 10 <span>README</span></div>
1057
+ </div>
1058
+ </section>
1059
+
1060
+ <!-- ═════════════════════ Definition of Done ═════════════════════ -->
1061
+ <section id="done" style="margin-top:48px;">
1062
+ <h2 style="font-size:20px;font-weight:800;margin-bottom:6px;color:#0f172a;">Definition of Done</h2>
1063
+ <p style="color:#64748b;font-size:14px;margin-bottom:20px;">Tick each item off as you complete it. Submission is ready only when all boxes are checked.</p>
1064
+ <ul class="checklist">
1065
+ <li><input type="checkbox" id="c1"><label for="c1">Notebook runs end-to-end in Colab without errors (no missing functions)</label></li>
1066
+ <li><input type="checkbox" id="c2"><label for="c2">Notebook sections are numbered and ordered correctly (Β§1 through Β§10)</label></li>
1067
+ <li><input type="checkbox" id="c3"><label for="c3">Training uses real CASIA v2 data β€” not synthetic noise</label></li>
1068
+ <li><input type="checkbox" id="c4"><label for="c4">Accuracy is in a realistic range (75–92%) β€” <strong>not 100%</strong></label></li>
1069
+ <li><input type="checkbox" id="c5"><label for="c5">Class imbalance is handled via class weights in <code>model.fit()</code></label></li>
1070
+ <li><input type="checkbox" id="c6"><label for="c6">Evaluation section includes: confusion matrix, precision, recall, F1, ROC-AUC</label></li>
1071
+ <li><input type="checkbox" id="c7"><label for="c7">Ablation study table compares M1, M2, M3 across all metrics</label></li>
1072
+ <li><input type="checkbox" id="c8"><label for="c8">Literature comparison table present with at least two published baselines</label></li>
1073
+ <li><input type="checkbox" id="c9"><label for="c9"><code>train.py</code> trains all three models (M1, M2, M3)</label></li>
1074
+ <li><input type="checkbox" id="c10"><label for="c10"><code>M3_best.keras</code> in the repo was trained on real CASIA v2 data</label></li>
1075
+ <li><input type="checkbox" id="c11"><label for="c11"><code>Documents/</code> folder contains the project report</label></li>
1076
+ <li><input type="checkbox" id="c12"><label for="c12"><code>README.md</code> explains the Gradio vs Streamlit difference</label></li>
1077
+ <li><input type="checkbox" id="c13"><label for="c13">Hugging Face Space redeployed with the new model weights</label></li>
1078
+ </ul>
1079
+ </section>
1080
+
1081
+ </main>
1082
+
1083
+ </body>
1084
+ </html>
notebooks/Image_Forgery_Detection_Colab_1.ipynb ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:39ffaa336055dc8cf27a970794c38afac258963d5d59016acde6d15b9386ab6b
3
+ size 34823
notebooks/Image_Forgery_Training_Notebook.ipynb ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8a409f96d3131069ee9f2a5ccd09d8900506a768479a361278696317cc986854
3
+ size 168823
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,8 @@
 
 
 
 
 
 
 
 
 
1
+ streamlit==1.35.0
2
+ tensorflow==2.15.0
3
+ opencv-python-headless==4.8.1.78
4
+ pillow==10.0.0
5
+ numpy==1.24.3
6
+ scikit-learn==1.3.2
7
+ matplotlib==3.8.0
8
+ huggingface-hub==0.19.4
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")