Commit Β·
55ae7dd
0
Parent(s):
Clean deployment: model loaded from HF Hub at runtime
Browse filesCo-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- .gitattributes +3 -0
- .gitignore +43 -0
- CLAUDE.md +55 -0
- README.md +33 -0
- app.py +188 -0
- documents/Project_Report_Digital_Image_Forgery_Detector.docx +3 -0
- documents/REMEDIATION_PLAN.md +450 -0
- documents/remediation_plan.html +1084 -0
- notebooks/Image_Forgery_Detection_Colab_1.ipynb +3 -0
- notebooks/Image_Forgery_Training_Notebook.ipynb +3 -0
- packages.txt +8 -0
- requirements.txt +8 -0
- train.py +172 -0
.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 & 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 & 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 & 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 & 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 & 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")
|