CoolFace
Apppublic

Psychomods/forensic-lab

sourceHugging Facemitupdated 7mo agoView on Hugging Face
0likes
streamlit_app.py1656 linesDownload Raw Back to src
1"""2╔══════════════════════════════════════════════════════════════════════╗3║        PSYCHOMODS CYBER FORENSIC LAB  –  v3.0                       ║4║        Advanced Image Manipulation Detection System                  ║5║        https://psychomods.infy.uk                                    ║6╠══════════════════════════════════════════════════════════════════════╣7║  Install:                                                            ║8║    pip install streamlit pillow opencv-python-headless numpy         ║9║                matplotlib reportlab scipy scikit-image plotly        ║10║                mediapipe                                             ║11╚══════════════════════════════════════════════════════════════════════╝12"""13 14# ── stdlib ─────────────────────────────────────────────────────────────15import io, math, time, hashlib, zipfile, json, os, struct, re16from datetime import datetime, timezone17from collections import defaultdict18 19# ── third-party ────────────────────────────────────────────────────────20import cv221import numpy as np22import matplotlib23matplotlib.use("Agg")24import matplotlib.pyplot as plt25import matplotlib.patches as mpatches26from matplotlib.gridspec import GridSpec27import plotly.graph_objects as go28import plotly.express as px29from PIL import Image, ImageChops, ImageEnhance, ImageFilter, ExifTags30from scipy.ndimage import uniform_filter, gaussian_filter31# scipy.stats unused imports removed32from skimage.feature import local_binary_pattern33# estimate_sigma replaced with pure-numpy version for compatibility34# skimage.measure unused35from reportlab.platypus import (36    SimpleDocTemplate, Paragraph, Spacer, Table,37    TableStyle, HRFlowable, KeepTogether38)39from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle40from reportlab.lib import colors as rl_colors41from reportlab.lib.units import mm42from reportlab.lib.pagesizes import A443import streamlit as st44import streamlit.components.v1 as components45 46# ── optional mediapipe ─────────────────────────────────────────────────47try:48    import mediapipe as mp49    MP_AVAILABLE = True50except ImportError:51    MP_AVAILABLE = False52 53# ══════════════════════════════════════════════════════════════════════54# PAGE CONFIG55# ══════════════════════════════════════════════════════════════════════56st.set_page_config(57    page_title="Psychomods Cyber Forensic Lab v3",58    page_icon="🧠",59    layout="wide",60    initial_sidebar_state="expanded",61)62 63# ══════════════════════════════════════════════════════════════════════64# MATRIX RAIN65# ══════════════════════════════════════════════════════════════════════66components.html("""67<canvas id="mx"></canvas>68<style>69  #mx{position:fixed;top:0;left:0;width:100vw;height:100vh;70      z-index:-1;pointer-events:none;background:#000;}71</style>72<script>73  const c=document.getElementById("mx"),ctx=c.getContext("2d");74  function rs(){c.width=innerWidth;c.height=innerHeight;}75  rs(); window.addEventListener("resize",rs);76  const CH="01アイウエオカキサシスセソABCDEF01";77  let drops=[];78  function init(){drops=Array(Math.ceil(c.width/13)).fill(1);}79  init(); window.addEventListener("resize",init);80  function draw(){81    ctx.fillStyle="rgba(0,0,0,0.05)";82    ctx.fillRect(0,0,c.width,c.height);83    ctx.font="13px monospace";84    drops.forEach((y,i)=>{85      ctx.fillStyle=i%5===0?"#ffffff":"#00ff9c";86      ctx.fillText(CH[Math.floor(Math.random()*CH.length)],i*13,y*13);87      if(y*13>c.height&&Math.random()>.975)drops[i]=0;88      drops[i]++;89    });90  }91  setInterval(draw,35);92</script>93""", height=0, width=0)94 95# ══════════════════════════════════════════════════════════════════════96# GLOBAL CSS97# ══════════════════════════════════════════════════════════════════════98st.markdown("""99<style>100html,body,[data-testid="stAppViewContainer"]{background:transparent!important;}101[data-testid="stAppViewContainer"]>.main{background:rgba(0,0,0,0.6);}102[data-testid="stSidebar"]{background:rgba(0,0,0,0.88)!important;}103::-webkit-scrollbar{width:5px;}104::-webkit-scrollbar-thumb{background:#00ff9c;border-radius:3px;}105 106.hdr{background:rgba(0,0,0,0.85);backdrop-filter:blur(20px);107  border-bottom:1px solid rgba(0,255,156,.5);padding:24px 30px 18px;108  border-radius:0 0 20px 20px;margin-bottom:24px;text-align:center;}109.hdr-t{font-size:38px;font-weight:900;letter-spacing:4px;color:#00ff9c;110  text-shadow:0 0 24px #00ff9c99;font-family:'Courier New',monospace;}111.hdr-s{color:#9affd7;font-size:12px;letter-spacing:2px;margin-top:4px;}112 113.sec{font-size:15px;font-weight:700;color:#00ff9c;letter-spacing:2px;114  border-left:4px solid #00ff9c;padding:6px 0 6px 14px;margin:28px 0 12px;115  background:rgba(0,255,156,.06);border-radius:0 8px 8px 0;116  font-family:'Courier New',monospace;}117 118.glass{background:rgba(0,0,0,.45);backdrop-filter:blur(18px);119  border:1px solid rgba(0,255,156,.3);border-radius:14px;padding:20px;margin-bottom:12px;}120 121.vcard{text-align:center;padding:18px;border-radius:12px;122  border:1px solid rgba(0,255,156,.3);background:rgba(0,0,0,.5);}123 124.mrow{display:flex;gap:8px;flex-wrap:wrap;margin:8px 0;}125.pill{background:rgba(0,255,156,.08);border:1px solid rgba(0,255,156,.3);126  border-radius:8px;padding:6px 12px;font-family:'Courier New',monospace;127  font-size:12px;color:#9affd7;}128.pill b{color:#00ff9c;}129 130.warn{background:rgba(255,100,0,.12);border:1px solid rgba(255,100,0,.4);131  border-radius:8px;padding:10px 14px;color:#ffaa66;font-size:13px;}132.ok{background:rgba(0,200,100,.1);border:1px solid rgba(0,200,100,.4);133  border-radius:8px;padding:10px 14px;color:#66ffaa;font-size:13px;}134 135div[data-testid="stFileUploader"]{136  border:1px dashed rgba(0,255,156,.4)!important;137  border-radius:12px;padding:8px;background:rgba(0,0,0,.3);}138div.stButton>button{139  background:linear-gradient(135deg,#00ff9c22,#00cc7a33);140  color:#00ff9c;border:1px solid #00ff9c;border-radius:10px;141  font-weight:700;font-family:'Courier New',monospace;142  letter-spacing:1px;padding:10px 26px;transition:all .25s;}143div.stButton>button:hover{background:#00ff9c;color:#000;box-shadow:0 0 18px #00ff9c88;}144div[data-testid="stProgress"]>div>div{145  background:linear-gradient(90deg,#00ff9c,#00cc7a)!important;}146label,.stMarkdown p{color:#c8ffe8!important;}147div[data-testid="stExpander"]{148  border:1px solid rgba(0,255,156,.25)!important;149  border-radius:10px;background:rgba(0,0,0,.35);}150.stTabs [data-baseweb="tab"]{color:#9affd7;font-family:'Courier New',monospace;}151.stTabs [aria-selected="true"]{color:#00ff9c!important;}152[data-testid="stMetric"]{153  background:rgba(0,255,156,.06);border:1px solid rgba(0,255,156,.2);154  border-radius:10px;padding:10px;}155[data-testid="stMetricValue"]{color:#00ff9c!important;}156[data-testid="stMetricLabel"]{color:#9affd7!important;}157 158.footer{position:fixed;bottom:0;left:0;width:100%;159  background:rgba(0,0,0,.82);backdrop-filter:blur(15px);160  border-top:1px solid rgba(0,255,156,.35);padding:9px;161  text-align:center;color:#00ff9c;font-size:11px;162  font-family:'Courier New',monospace;z-index:9999;}163html{scroll-behavior:smooth;}164</style>165<div class="hdr">166  <div class="hdr-t">🧠 PSYCHOMODS CYBER FORENSIC LAB</div>167  <div class="hdr-s">⚡ v3.0 · Advanced Image Manipulation Detection System ⚡</div>168</div>169""", unsafe_allow_html=True)170 171 172# ══════════════════════════════════════════════════════════════════════173# UTILITIES174# ══════════════════════════════════════════════════════════════════════175def sec(title: str):176    st.markdown(f'<div class="sec">▶ {title}</div>', unsafe_allow_html=True)177 178def pill_row(pairs: list):179    html = '<div class="mrow">'180    for k, v in pairs:181        html += f'<div class="pill">{k} <b>{v}</b></div>'182    html += '</div>'183    st.markdown(html, unsafe_allow_html=True)184 185def pil_to_cv(img: Image.Image) -> np.ndarray:186    return cv2.cvtColor(np.array(img.convert("RGB")), cv2.COLOR_RGB2BGR)187 188def cv_to_pil(img: np.ndarray) -> Image.Image:189    if len(img.shape) == 2:190        return Image.fromarray(img)191    return Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))192 193def fig_to_pil(fig) -> Image.Image:194    buf = io.BytesIO()195    fig.savefig(buf, format="png", bbox_inches="tight", facecolor="#000", dpi=110)196    buf.seek(0); plt.close(fig)197    return Image.open(buf).copy()198 199def dark_fig(w=5, h=3):200    fig, ax = plt.subplots(figsize=(w, h), facecolor="#000")201    ax.set_facecolor("#000")202    for sp in ax.spines.values(): sp.set_edgecolor("#00ff9c33")203    ax.tick_params(colors="#9affd7", labelsize=8)204    ax.xaxis.label.set_color("#9affd7")205    ax.yaxis.label.set_color("#9affd7")206    return fig, ax207 208def hash_bytes(data: bytes):209    return hashlib.sha256(data).hexdigest(), hashlib.md5(data).hexdigest()210 211 212# ══════════════════════════════════════════════════════════════════════213# ── MODULE 1 · ELA ────────────────────────────────────────────────────214# ══════════════════════════════════════════════════════════════════════215def ela(img: Image.Image, quality=92, scale=20) -> Image.Image:216    rgb = img.convert("RGB")217    buf = io.BytesIO()218    rgb.save(buf, "JPEG", quality=quality); buf.seek(0)219    diff = ImageChops.difference(rgb, Image.open(buf))220    return ImageEnhance.Brightness(diff).enhance(scale)221 222def ela_heatmap(e: Image.Image) -> Image.Image:223    gray = cv2.cvtColor(np.array(e.convert("RGB")), cv2.COLOR_RGB2GRAY)224    return cv_to_pil(cv2.applyColorMap(gray, cv2.COLORMAP_JET))225 226def ela_stats(e: Image.Image):227    a = np.array(e.convert("RGB")).astype(float)228    mean = float(np.mean(a)); std = float(np.std(a))229    p95  = float(np.percentile(a, 95))230    prob = min(100.0, mean * 2.2)231    if   mean < 8:  v,c = "LIKELY AUTHENTIC",      "#22c55e"232    elif mean < 18: v,c = "MINOR EDITING POSSIBLE", "#84cc16"233    elif mean < 30: v,c = "POSSIBLY EDITED",        "#facc15"234    elif mean < 45: v,c = "SUSPICIOUS / EDITED",    "#f97316"235    else:           v,c = "LIKELY MANIPULATED",     "#ef4444"236    return mean, std, p95, prob, v, c237 238def ela_region_detect(img: Image.Image, e: Image.Image,239                      thresh=20, min_area=300):240    out  = pil_to_cv(img)241    gray = cv2.cvtColor(np.array(e.convert("RGB")), cv2.COLOR_RGB2GRAY)242    _, th = cv2.threshold(gray, thresh, 255, cv2.THRESH_BINARY)243    k    = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))244    th   = cv2.morphologyEx(th, cv2.MORPH_CLOSE, k, iterations=2)245    cnts, _ = cv2.findContours(th, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)246    n = 0247    for c in cnts:248        if cv2.contourArea(c) > min_area:249            x,y,w,h = cv2.boundingRect(c)250            cv2.rectangle(out,(x,y),(x+w,y+h),(0,0,255),2)251            cv2.putText(out,f"#{n+1}",(x+4,y+18),252                        cv2.FONT_HERSHEY_SIMPLEX,.55,(0,0,255),1)253            n += 1254    cv2.putText(out,f"Suspicious: {n}",(8,22),255                cv2.FONT_HERSHEY_SIMPLEX,.6,(0,255,156),2)256    return cv_to_pil(out), n257 258 259# ══════════════════════════════════════════════════════════════════════260# ── MODULE 2 · CLONE / COPY-MOVE ─────────────────────────────────────261# ══════════════════════════════════════════════════════════════════════262def clone_detect(img: Image.Image):263    gray = cv2.cvtColor(np.array(img.convert("RGB")), cv2.COLOR_RGB2GRAY)264    orb  = cv2.ORB_create(nfeatures=8000)265    kp, des = orb.detectAndCompute(gray, None)266    out  = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)267    if des is None or len(kp) < 10:268        return cv_to_pil(out), 0269    bf   = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)270    seen = set(); n = 0271    for m in sorted(bf.match(des,des), key=lambda x:x.distance):272        qi, ti = m.queryIdx, m.trainIdx273        if qi == ti: continue274        key = (min(qi,ti),max(qi,ti))275        if key in seen: continue276        seen.add(key)277        p1 = tuple(map(int, kp[qi].pt))278        p2 = tuple(map(int, kp[ti].pt))279        if math.hypot(p1[0]-p2[0],p1[1]-p2[1]) > 20:280            cv2.line(out, p1, p2, (0,255,100), 1)281            cv2.circle(out, p1, 3, (0,200,255), -1)282            cv2.circle(out, p2, 3, (255,80,0),  -1)283            n += 1284        if n >= 40: break285    cv2.putText(out,f"Clone pairs: {n}",(8,22),286                cv2.FONT_HERSHEY_SIMPLEX,.6,(0,255,156),2)287    return cv_to_pil(out), n288 289 290# ══════════════════════════════════════════════════════════════════════291# ── MODULE 3 · NOISE RESIDUAL ─────────────────────────────────────────292# ══════════════════════════════════════════════════════════════════════293def _noise_sigma_mad(arr: np.ndarray) -> float:294    """Estimate noise sigma via Median Absolute Deviation on the Laplacian.295    Pure numpy — no skimage.restoration dependency."""296    sigmas = []297    for c in range(arr.shape[2]):298        lap = cv2.Laplacian(arr[:,:,c], cv2.CV_32F)299        mad = float(np.median(np.abs(lap - np.median(lap))))300        sigmas.append(mad / 0.6745)301    return float(np.mean(sigmas))302 303def noise_residual(img: Image.Image):304    arr  = np.array(img.convert("RGB")).astype(np.float32)305    blur = cv2.GaussianBlur(arr,(5,5),0)306    res  = np.clip(np.abs(arr-blur)*4,0,255).astype(np.uint8)307    sig  = _noise_sigma_mad(arr)308    heat = cv2.applyColorMap(cv2.cvtColor(res,cv2.COLOR_RGB2GRAY), cv2.COLORMAP_HOT)309    cv2.putText(heat,f"σ={sig:.2f}",(8,22),cv2.FONT_HERSHEY_SIMPLEX,.6,(0,255,156),2)310    return cv_to_pil(heat), sig311 312 313# ══════════════════════════════════════════════════════════════════════314# ── MODULE 4 · EDGE ANALYSIS ─────────────────────────────────────────315# ══════════════════════════════════════════════════════════════════════316def edge_analysis(img: Image.Image):317    gray  = cv2.cvtColor(np.array(img.convert("RGB")),cv2.COLOR_RGB2GRAY)318    edges = cv2.Canny(cv2.GaussianBlur(gray,(3,3),0), 80, 180)319    density = float(np.count_nonzero(edges))/edges.size*100320    out   = np.zeros((*edges.shape,3),dtype=np.uint8)321    out[edges>0] = [0,255,156]322    cv2.putText(out,f"Density: {density:.1f}%",(8,22),323                cv2.FONT_HERSHEY_SIMPLEX,.6,(255,200,0),2)324    return cv_to_pil(out), density325 326 327# ══════════════════════════════════════════════════════════════════════328# ── MODULE 5 · FFT FREQUENCY SPECTRUM ────────────────────────────────329# ══════════════════════════════════════════════════════════════════════330def fft_analysis(img: Image.Image) -> Image.Image:331    gray = cv2.cvtColor(np.array(img.convert("RGB")),cv2.COLOR_RGB2GRAY).astype(np.float32)332    mag  = 20*np.log1p(np.abs(np.fft.fftshift(np.fft.fft2(gray))))333    norm = cv2.normalize(mag,None,0,255,cv2.NORM_MINMAX).astype(np.uint8)334    return cv_to_pil(cv2.applyColorMap(norm, cv2.COLORMAP_INFERNO))335 336 337# ══════════════════════════════════════════════════════════════════════338# ── MODULE 6 · DCT BLOCK ANALYSIS ────────────────────────────────────339# ══════════════════════════════════════════════════════════════════════340def dct_analysis(img: Image.Image):341    gray = cv2.cvtColor(np.array(img.convert("RGB")),cv2.COLOR_RGB2GRAY).astype(np.float32)342    h,w  = gray.shape343    bh,bw = h-h%8, w-w%8344    gray  = gray[:bh,:bw]345    emap  = np.zeros_like(gray)346    bmeans = []347    for r in range(0,bh,8):348        for cc in range(0,bw,8):349            blk = gray[r:r+8,cc:cc+8]350            d   = cv2.dct(blk)351            e   = float(np.log1p(np.abs(d)).mean())352            emap[r:r+8,cc:cc+8] = e353            bmeans.append(e)354    score = float(np.std(bmeans))355    norm  = cv2.normalize(emap,None,0,255,cv2.NORM_MINMAX).astype(np.uint8)356    heat  = cv2.applyColorMap(norm, cv2.COLORMAP_PLASMA)357    cv2.putText(heat,f"DCT inconsistency: {score:.2f}",(8,22),358                cv2.FONT_HERSHEY_SIMPLEX,.55,(0,255,156),2)359    return cv_to_pil(heat), score360 361 362# ══════════════════════════════════════════════════════════════════════363# ── MODULE 7 · LBP TEXTURE ───────────────────────────────────────────364# ══════════════════════════════════════════════════════════════════════365def lbp_analysis(img: Image.Image) -> Image.Image:366    gray = cv2.cvtColor(np.array(img.convert("RGB")),cv2.COLOR_RGB2GRAY)367    lbp  = local_binary_pattern(gray, P=8, R=1.0, method="uniform")368    norm = cv2.normalize(lbp,None,0,255,cv2.NORM_MINMAX).astype(np.uint8)369    return cv_to_pil(cv2.applyColorMap(norm, cv2.COLORMAP_TWILIGHT_SHIFTED))370 371 372# ══════════════════════════════════════════════════════════════════════373# ── MODULE 8 · JPEG GHOST ────────────────────────────────────────────374# ══════════════════════════════════════════════════════════════════════375def jpeg_ghost(img: Image.Image, qualities=(50,70,85,95)) -> Image.Image:376    orig = np.array(img.convert("RGB")).astype(np.float32)377    maps = []378    for q in qualities:379        buf = io.BytesIO()380        img.convert("RGB").save(buf,"JPEG",quality=q); buf.seek(0)381        comp = np.array(Image.open(buf)).astype(np.float32)382        maps.append(np.abs(orig-comp).mean(axis=2))383    norm = cv2.normalize(np.mean(maps,axis=0),None,0,255,cv2.NORM_MINMAX).astype(np.uint8)384    return cv_to_pil(cv2.applyColorMap(norm, cv2.COLORMAP_OCEAN))385 386 387# ══════════════════════════════════════════════════════════════════════388# ── MODULE 9 · LUMINANCE GRADIENT ────────────────────────────────────389# ══════════════════════════════════════════════════════════════════════390def luminance_gradient(img: Image.Image) -> Image.Image:391    gray = cv2.cvtColor(np.array(img.convert("RGB")),cv2.COLOR_RGB2GRAY).astype(np.float32)392    mag  = cv2.magnitude(cv2.Sobel(gray,cv2.CV_32F,1,0,ksize=3),393                         cv2.Sobel(gray,cv2.CV_32F,0,1,ksize=3))394    norm = cv2.normalize(mag,None,0,255,cv2.NORM_MINMAX).astype(np.uint8)395    return cv_to_pil(cv2.applyColorMap(norm, cv2.COLORMAP_MAGMA))396 397 398# ══════════════════════════════════════════════════════════════════════399# ── MODULE 10 · RGB HISTOGRAM ────────────────────────────────────────400# ══════════════════════════════════════════════════════════════════════401def histogram_plot(img: Image.Image) -> Image.Image:402    arr = np.array(img.convert("RGB"))403    fig, ax = dark_fig(5,3)404    for name,idx,col in [("R",0,"#ff4444"),("G",1,"#44ff88"),("B",2,"#4488ff")]:405        ax.plot(cv2.calcHist([arr],[idx],None,[256],[0,256]).flatten(),406                color=col, alpha=.85, lw=1.2, label=name)407    ax.legend(facecolor="#111",edgecolor="#00ff9c",labelcolor="#fff",fontsize=8)408    ax.set_xlim(0,255); ax.set_title("RGB Histogram",color="#00ff9c",fontsize=10)409    return fig_to_pil(fig)410 411 412# ══════════════════════════════════════════════════════════════════════413# ── MODULE 11 · STEGANOGRAPHY DETECTION ──────────────────────────────414# ══════════════════════════════════════════════════════════════════════415def steg_lsb_planes(img: Image.Image):416    """Extract all 8 bit-planes per channel and visualise the LSB plane."""417    arr  = np.array(img.convert("RGB"))418    # LSB plane (bit 0)419    lsb  = (arr & 1) * 255420    out  = lsb.astype(np.uint8)421    return Image.fromarray(out)422 423def steg_chi_square(img: Image.Image):424    """425    Chi-square attack on LSB steganography.426    Returns (score, p_value, is_suspicious, message).427    High chi-square score → uniform LSB distribution → hidden data likely.428    """429    arr  = np.array(img.convert("RGB")).flatten()430    # pair of value groups: (0,1),(2,3),(4,5),...431    scores = []432    for ch_start in range(0, len(arr), len(arr)//3):433        ch = arr[ch_start:ch_start+len(arr)//3]434        observed = np.bincount(ch, minlength=256).astype(float)435        # Chi-square on even/odd pairs436        pairs_obs = []437        pairs_exp = []438        for i in range(0, 256, 2):439            total = observed[i]+observed[i+1]440            pairs_obs.extend([observed[i], observed[i+1]])441            pairs_exp.extend([total/2, total/2])442        pairs_obs = np.array(pairs_obs)443        pairs_exp = np.array(pairs_exp)444        mask = pairs_exp > 0445        chi2 = float(np.sum((pairs_obs[mask]-pairs_exp[mask])**2/pairs_exp[mask]))446        scores.append(chi2)447    avg_chi2 = float(np.mean(scores))448    # normalise to 0-100 suspicion score (lower chi2 = more suspicious)449    susp = max(0.0, 100.0 - min(avg_chi2/5000*100, 100))450    suspicious = susp > 60451    return avg_chi2, susp, suspicious452 453def steg_entropy_map(img: Image.Image) -> Image.Image:454    """Local entropy map — high entropy blobs in LSB plane indicate hidden data."""455    gray = cv2.cvtColor(np.array(img.convert("RGB")),cv2.COLOR_RGB2GRAY)456    lsb  = (gray & 1).astype(np.float32)457    # sliding window entropy via uniform filter on squared values458    emap = np.zeros_like(lsb)459    win  = 16460    p    = uniform_filter(lsb, size=win)461    p    = np.clip(p, 1e-7, 1.0 - 1e-7)  # keep away from 0/1 boundaries462    with np.errstate(divide="ignore", invalid="ignore"):463        emap = -(p * np.log2(p) + (1 - p) * np.log2(1 - p))464    emap = np.nan_to_num(emap, nan=0.0, posinf=0.0, neginf=0.0).astype(np.float32)465    norm = cv2.normalize(emap, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)466    heat = cv2.applyColorMap(norm, cv2.COLORMAP_VIRIDIS)467    return cv_to_pil(heat)468 469def steg_bit_planes(img: Image.Image) -> Image.Image:470    """Show all 8 bit-planes of the grayscale image in a grid."""471    gray = cv2.cvtColor(np.array(img.convert("RGB")),cv2.COLOR_RGB2GRAY)472    fig, axes = plt.subplots(2,4,figsize=(10,5),facecolor="#000")473    for bit in range(8):474        ax = axes[bit//4][bit%4]475        plane = ((gray >> bit) & 1)*255476        ax.imshow(plane, cmap="gray", vmin=0, vmax=255)477        ax.set_title(f"Bit {bit}", color="#00ff9c", fontsize=9)478        ax.axis("off")479        ax.set_facecolor("#000")480    fig.patch.set_facecolor("#000")481    plt.tight_layout()482    return fig_to_pil(fig)483 484 485# ══════════════════════════════════════════════════════════════════════486# ── MODULE 12 · METADATA TAMPERING TIMELINE ──────────────────────────487# ══════════════════════════════════════════════════════════════════════488def extract_exif_full(img: Image.Image) -> dict:489    try:490        raw = img._getexif()491        if not raw: return {}492        out = {}493        for tid, val in raw.items():494            tag = ExifTags.TAGS.get(tid, str(tid))495            if isinstance(val, bytes):496                try:    val = val.decode("utf-8","replace")497                except: val = val.hex()498            out[tag] = str(val)[:300]499        return out500    except Exception:501        return {}502 503def metadata_timeline(exif: dict) -> tuple:504    """505    Cross-check EXIF dates and software fields for inconsistencies.506    Returns (events list, anomalies list, risk_score 0-100).507    """508    DATE_TAGS   = ["DateTime","DateTimeOriginal","DateTimeDigitized",509                   "GPSDateStamp","CreateDate","ModifyDate"]510    SW_TAGS     = ["Software","ProcessingSoftware","HostComputer"]511    EDITING_KW  = ["photoshop","lightroom","gimp","affinity","capture",512                   "adobe","snapseed","vsco","pixelmator","paint.net",513                   "darktable","rawtherapee","luminar","canva"]514 515    events    = []516    anomalies = []517    dates     = {}518    software  = []519 520    for tag in DATE_TAGS:521        if tag in exif:522            val = exif[tag].strip()523            events.append({"tag": tag, "value": val, "type": "date"})524            try:525                # EXIF datetime: "YYYY:MM:DD HH:MM:SS"526                dt = datetime.strptime(val[:19], "%Y:%m:%d %H:%M:%S")527                dates[tag] = dt528            except Exception:529                pass530 531    for tag in SW_TAGS:532        if tag in exif:533            val = exif[tag].strip()534            software.append(val)535            events.append({"tag": tag, "value": val, "type": "software"})536 537    # GPS538    if "GPSInfo" in exif:539        events.append({"tag":"GPSInfo","value":"GPS coordinates present","type":"gps"})540 541    # Anomaly: editing software detected542    for sw in software:543        for kw in EDITING_KW:544            if kw in sw.lower():545                anomalies.append(f"Editing software detected: '{sw}'")546                break547 548    # Anomaly: date inconsistencies549    if len(dates) >= 2:550        vals = list(dates.values())551        for i in range(len(vals)):552            for j in range(i+1, len(vals)):553                diff = abs((vals[i]-vals[j]).total_seconds())554                if diff > 86400:  # >1 day difference555                    anomalies.append(556                        f"Date mismatch: {list(dates.keys())[i]} vs "557                        f"{list(dates.keys())[j]} differ by "558                        f"{diff/3600:.1f} hours")559 560    # Anomaly: future dates561    now_naive = datetime.now()  # naive, matches EXIF parsed datetimes562    for tag, dt in dates.items():563        if dt > now_naive:564            anomalies.append(f"Future date in {tag}: {dt}")565 566    # Anomaly: year 1970 / epoch567    for tag, dt in dates.items():568        if dt.year < 2000:569            anomalies.append(f"Suspicious early date in {tag}: {dt.year}")570 571    # Risk score572    risk = min(100, len(anomalies)*25 + (10 if software else 0))573    return events, anomalies, risk574 575 576# ══════════════════════════════════════════════════════════════════════577# ── MODULE 13 · SPLICING BOUNDARY DETECTION ──────────────────────────578# ══════════════════════════════════════════════════════════════════════579def splicing_boundary(img: Image.Image) -> tuple:580    """581    Detect splicing boundaries using:582    1. Noise inconsistency map (block-wise σ)583    2. Illumination gradient discontinuity584    3. Combined heatmap585    Returns (heatmap_pil, splice_score 0-100)586    """587    arr  = np.array(img.convert("RGB")).astype(np.float32)588    gray = cv2.cvtColor(arr.astype(np.uint8), cv2.COLOR_RGB2GRAY).astype(np.float32)589    h, w = gray.shape590 591    # ── Block-wise noise sigma map ──────────────────────────────────592    bsize  = 32593    nmap   = np.zeros((h, w), dtype=np.float32)594    sigmas = []595    for r in range(0, h-bsize, bsize//2):596        for cc in range(0, w-bsize, bsize//2):597            blk = gray[r:r+bsize, cc:cc+bsize]598            # noise estimate: median absolute deviation of Laplacian599            lap = cv2.Laplacian(blk, cv2.CV_32F)600            s   = float(np.median(np.abs(lap - np.median(lap)))) / 0.6745601            nmap[r:r+bsize, cc:cc+bsize] = s602            sigmas.append(s)603 604    # ── Illumination map (local mean luminance) ─────────────────────605    illum  = cv2.GaussianBlur(gray, (31,31), 0)606    # gradient of illumination607    gx = cv2.Sobel(illum, cv2.CV_32F, 1, 0, ksize=5)608    gy = cv2.Sobel(illum, cv2.CV_32F, 0, 1, ksize=5)609    illum_grad = cv2.magnitude(gx, gy)610 611    # ── Combine ─────────────────────────────────────────────────────612    noise_norm  = cv2.normalize(nmap,       None, 0, 1, cv2.NORM_MINMAX)613    illum_norm  = cv2.normalize(illum_grad, None, 0, 1, cv2.NORM_MINMAX)614    combined    = (noise_norm * 0.6 + illum_norm * 0.4)615    combined_n  = cv2.normalize(combined,   None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)616    heat        = cv2.applyColorMap(combined_n, cv2.COLORMAP_JET)617 618    # splice score: std of block sigmas (high std = inconsistent noise = spliced)619    splice_score = min(100.0, float(np.std(sigmas)) * 15)620 621    cv2.putText(heat, f"Splice score: {splice_score:.1f}",622                (8,22), cv2.FONT_HERSHEY_SIMPLEX, .6, (0,255,156), 2)623    return cv_to_pil(heat), splice_score624 625 626# ══════════════════════════════════════════════════════════════════════627# ── MODULE 14 · GAN / AI ARTIFACT DETECTION ──────────────────────────628# ══════════════════════════════════════════════════════════════════════629def gan_checkerboard(img: Image.Image):630    """631    Detect checkerboard artifacts in FFT — hallmark of transposed-convolution632    GAN generators (DCGAN, StyleGAN upsampling artefacts).633    Returns (annotated_fft_pil, cb_score 0-100, peaks_found int).634    """635    gray = cv2.cvtColor(np.array(img.convert("RGB")),cv2.COLOR_RGB2GRAY).astype(np.float32)636    f    = np.fft.fftshift(np.fft.fft2(gray))637    mag  = np.log1p(np.abs(f))638    h, w = mag.shape639    cx, cy = w//2, h//2640 641    # Normalise for display642    disp = cv2.normalize(mag,None,0,255,cv2.NORM_MINMAX).astype(np.uint8)643    heat = cv2.applyColorMap(disp, cv2.COLORMAP_INFERNO)644 645    # Look for periodic peaks at N/2, N/4 frequencies (GAN checkerboard)646    # Sample horizontal and vertical lines through centre647    h_line = mag[cy, :]648    v_line = mag[:, cx]649    peaks_found = 0650    cb_score    = 0.0651 652    for line in [h_line, v_line]:653        half  = len(line)//2654        # Check N/2 frequency spike655        n2    = half//2656        local = line[max(0,n2-3):n2+3]657        surr  = np.concatenate([line[max(0,n2-20):max(0,n2-4)],658                                 line[n2+4:n2+20]])659        if len(surr) > 0 and local.max() > surr.mean()*1.5:660            peaks_found += 1661            cv2.line(heat,(0,cy),(w,cy),(0,255,156),1)662            cv2.line(heat,(cx,0),(cx,h),(0,255,156),1)663 664    cb_score = min(100.0, peaks_found * 35.0 + float(np.std(mag))*0.5)665    cv2.putText(heat,f"GAN CB score: {cb_score:.1f}",666                (8,22),cv2.FONT_HERSHEY_SIMPLEX,.55,(0,255,156),2)667    return cv_to_pil(heat), cb_score, peaks_found668 669def gan_texture_variance(img: Image.Image):670    """671    GAN-generated images tend to have unnaturally uniform texture.672    Measure local variance map and its global std.673    Very low global std → suspiciously uniform → possible GAN.674    """675    gray  = cv2.cvtColor(np.array(img.convert("RGB")),cv2.COLOR_RGB2GRAY).astype(np.float32)676    # Local variance via box filter677    mu    = uniform_filter(gray,    size=15)678    mu2   = uniform_filter(gray**2, size=15)679    var   = np.clip(mu2 - mu**2, 0, None)680    gstd  = float(np.std(var))681    norm  = cv2.normalize(var,None,0,255,cv2.NORM_MINMAX).astype(np.uint8)682    heat  = cv2.applyColorMap(norm, cv2.COLORMAP_COOL)683    # GAN score: low gstd = uniform texture = suspicious684    gan_score = max(0.0, min(100.0, 100.0 - gstd * 0.8))685    cv2.putText(heat,f"Texture uniformity: {gan_score:.1f}",686                (8,22),cv2.FONT_HERSHEY_SIMPLEX,.55,(0,255,156),2)687    return cv_to_pil(heat), gan_score688 689def gan_color_inconsistency(img: Image.Image):690    """691    Detect unnatural colour distribution — GAN images often have692    shifted chrominance histograms and low inter-channel correlation.693    """694    arr = np.array(img.convert("RGB")).astype(np.float32)695    r, g, b = arr[:,:,0].flatten(), arr[:,:,1].flatten(), arr[:,:,2].flatten()696    # Correlation between channels (real photos tend to be highly correlated)697    corr_rg = float(np.corrcoef(r,g)[0,1])698    corr_rb = float(np.corrcoef(r,b)[0,1])699    corr_gb = float(np.corrcoef(g,b)[0,1])700    avg_corr = (corr_rg+corr_rb+corr_gb)/3701 702    # Build colour histogram plot703    fig, axes = plt.subplots(1,3,figsize=(9,2.5),facecolor="#000")704    for ax,(ch,col,name) in zip(axes,[(r,"#ff4444","R"),(g,"#44ff88","G"),(b,"#4488ff","B")]):705        ax.hist(ch, bins=64, color=col, alpha=.8, histtype="stepfilled")706        ax.set_facecolor("#000"); ax.set_title(name,color=col,fontsize=9)707        for sp in ax.spines.values(): sp.set_edgecolor("#333")708        ax.tick_params(colors="#9affd7",labelsize=7)709    fig.suptitle(f"Channel correlation: {avg_corr:.3f}",color="#00ff9c",fontsize=10)710    plt.tight_layout()711 712    # Score: very low or very high correlation can indicate GAN713    gan_score = max(0.0, min(100.0, (1.0-abs(avg_corr))*120))714    return fig_to_pil(fig), gan_score, avg_corr715 716def gan_frequency_fingerprint(img: Image.Image):717    """718    Real images follow a 1/f power spectrum.719    GAN images deviate — check slope of radially averaged power spectrum.720    """721    gray = cv2.cvtColor(np.array(img.convert("RGB")),cv2.COLOR_RGB2GRAY).astype(np.float32)722    f    = np.fft.fftshift(np.fft.fft2(gray))723    power= np.abs(f)**2724    h,w  = power.shape725    cy,cx= h//2, w//2726    max_r= min(cx,cy)727    radial_power = []728    for r in range(1,max_r,2):729        mask = np.zeros((h,w),dtype=bool)730        cv2.circle(mask.view(np.uint8),( cx,cy),r,1,-1)731        cv2.circle(mask.view(np.uint8),(cx,cy),max(0,r-2),0,-1)732        if mask.sum()>0:733            radial_power.append(float(power[mask].mean()))734 735    if len(radial_power) < 10:736        return None, 0.0737 738    rp   = np.array(radial_power)739    freq = np.arange(1,len(rp)+1,dtype=float)740    log_f= np.log10(freq); log_p = np.log10(rp+1)741    # Linear fit — slope should be ~-2 to -3 for natural images742    slope,intercept = np.polyfit(log_f, log_p, 1)743 744    fig, ax = dark_fig(5,3)745    ax.plot(log_f, log_p, color="#00ff9c", lw=1.5, label="Power spectrum")746    ax.plot(log_f, slope*log_f+intercept, "--",color="#ff4444",lw=1,label=f"Fit slope={slope:.2f}")747    ax.set_xlabel("log10(frequency)"); ax.set_ylabel("log10(power)")748    ax.set_title("Radial Power Spectrum",color="#00ff9c",fontsize=10)749    ax.legend(facecolor="#111",edgecolor="#00ff9c",labelcolor="#fff",fontsize=8)750 751    # GAN score: slope significantly different from natural range (-1.5 to -3.5)752    gan_score = 0.0753    if slope > -1.0 or slope < -5.0:754        gan_score = min(100.0, abs(slope + 2.5)*30)755    return fig_to_pil(fig), gan_score756 757 758# ══════════════════════════════════════════════════════════════════════759# ── MODULE 15 · FACE / FACIAL LANDMARK ANALYSIS ──────────────────────760# ══════════════════════════════════════════════════════════════════════761def face_analysis_cv(img: Image.Image):762    """763    Face detection + eye asymmetry check using OpenCV Haar cascades.764    Works without mediapipe or dlib.765    Returns (annotated_pil, results_dict).766    """767    out  = pil_to_cv(img)768    gray = cv2.cvtColor(out, cv2.COLOR_BGR2GRAY)769 770    face_cascade = cv2.CascadeClassifier(771        cv2.data.haarcascades + "haarcascade_frontalface_default.xml")772    eye_cascade  = cv2.CascadeClassifier(773        cv2.data.haarcascades + "haarcascade_eye.xml")774 775    faces = face_cascade.detectMultiScale(gray,1.1,5,minSize=(60,60))776    results = {"faces":len(faces),"eyes_per_face":[],"asymmetry_scores":[],777               "anomalies":[]}778 779    for (fx,fy,fw,fh) in faces:780        cv2.rectangle(out,(fx,fy),(fx+fw,fy+fh),(0,255,156),2)781        roi_gray = gray[fy:fy+fh, fx:fx+fw]782        roi_col  = out [fy:fy+fh, fx:fx+fw]783        eyes = eye_cascade.detectMultiScale(roi_gray,1.1,5,minSize=(20,20))784        results["eyes_per_face"].append(len(eyes))785 786        eye_centers = []787        for (ex,ey,ew,eh) in eyes:788            cv2.rectangle(roi_col,(ex,ey),(ex+ew,ey+eh),(255,100,0),1)789            eye_centers.append((ex+ew//2, ey+eh//2))790 791        # Eye asymmetry: vertical position difference792        if len(eye_centers)==2:793            ydiff = abs(eye_centers[0][1]-eye_centers[1][1])794            norm_ydiff = ydiff/fh*100795            results["asymmetry_scores"].append(norm_ydiff)796            if norm_ydiff > 8:797                results["anomalies"].append(798                    f"Eye vertical asymmetry: {norm_ydiff:.1f}% of face height")799            # Eye size comparison800            if len(eyes)==2:801                area0 = eyes[0][2]*eyes[0][3]802                area1 = eyes[1][2]*eyes[1][3]803                size_ratio = max(area0,area1)/max(min(area0,area1),1)804                if size_ratio > 1.6:805                    results["anomalies"].append(806                        f"Eye size ratio: {size_ratio:.2f}x (unusual)")807 808        cv2.putText(out,f"Face: {len(eyes)} eyes",809                    (fx,fy-8),cv2.FONT_HERSHEY_SIMPLEX,.5,(0,255,156),1)810 811    if len(faces)==0:812        cv2.putText(out,"No faces detected",(8,22),813                    cv2.FONT_HERSHEY_SIMPLEX,.6,(255,200,0),2)814 815    return cv_to_pil(out), results816 817def face_blending_boundary(img: Image.Image):818    """819    Detect face-swap blending seams using:820    - Local colour variance discontinuity around detected face boundary821    - High-pass filtered difference map822    """823    arr  = np.array(img.convert("RGB")).astype(np.float32)824    gray = cv2.cvtColor(arr.astype(np.uint8),cv2.COLOR_RGB2GRAY)825 826    # High-pass via unsharp masking827    blurred    = cv2.GaussianBlur(arr,(15,15),0)828    high_pass  = np.abs(arr - blurred)829    hp_gray    = high_pass.mean(axis=2)830 831    # Local std map832    local_mean = uniform_filter(hp_gray, size=20)833    local_sq   = uniform_filter(hp_gray**2, size=20)834    local_std  = np.sqrt(np.clip(local_sq-local_mean**2, 0, None))835 836    norm = cv2.normalize(local_std,None,0,255,cv2.NORM_MINMAX).astype(np.uint8)837    heat = cv2.applyColorMap(norm, cv2.COLORMAP_HOT)838 839    seam_score = min(100.0, float(np.std(local_std))*8)840    cv2.putText(heat,f"Seam score: {seam_score:.1f}",841                (8,22),cv2.FONT_HERSHEY_SIMPLEX,.6,(0,255,156),2)842    return cv_to_pil(heat), seam_score843 844 845# ══════════════════════════════════════════════════════════════════════846# ── MODULE 16 · PIXEL DIFF (single-image & two-image) ────────────────847# ══════════════════════════════════════════════════════════════════════848def pixel_diff(imgA: Image.Image, imgB: Image.Image, amplify=5):849    """Absolute pixel difference between two images (resized to match)."""850    sz = (min(imgA.width,imgB.width), min(imgA.height,imgB.height))851    a  = np.array(imgA.convert("RGB").resize(sz)).astype(np.int16)852    b  = np.array(imgB.convert("RGB").resize(sz)).astype(np.int16)853    diff = np.abs(a-b).astype(np.uint8)854    diff_amp = np.clip(diff*amplify,0,255).astype(np.uint8)855    mean_diff = float(np.mean(diff))856    heat = cv2.applyColorMap(857        cv2.cvtColor(diff_amp,cv2.COLOR_RGB2GRAY), cv2.COLORMAP_JET)858    cv2.putText(heat,f"Mean diff: {mean_diff:.2f}",859                (8,22),cv2.FONT_HERSHEY_SIMPLEX,.6,(0,255,156),2)860    return cv_to_pil(heat), mean_diff861 862 863# ══════════════════════════════════════════════════════════════════════864# ── MODULE 17 · FILE INFO & METADATA ─────────────────────────────────865# ══════════════════════════════════════════════════════════════════════866def file_info(uf, img: Image.Image) -> dict:867    data = uf.getvalue()868    sha, md5 = hash_bytes(data)869    return {870        "Filename"   : uf.name,871        "File size"  : f"{len(data)/1024:.1f} KB",872        "Format"     : img.format or "Unknown",873        "Mode"       : img.mode,874        "Resolution" : f"{img.width} × {img.height} px",875        "Megapixels" : f"{img.width*img.height/1e6:.2f} MP",876        "SHA-256"    : sha,877        "MD5"        : md5,878    }879 880 881# ══════════════════════════════════════════════════════════════════════882# ── MODULE 18 · COMPOSITE SCORE + RADAR CHART ────────────────────────883# ══════════════════════════════════════════════════════════════════════884def composite_score(ela_prob, clone_pairs, dct_score,885                    noise_sigma, edge_density, splice_score,886                    steg_susp, gan_cb, face_seam):887    w = {888        "ELA":       (0.25, min(ela_prob,       100)),889        "Clone":     (0.15, min(clone_pairs*2.5,100)),890        "DCT":       (0.12, min(dct_score*1.5,  100)),891        "Noise":     (0.08, min(noise_sigma*2,  100)),892        "Splice":    (0.15, min(splice_score,   100)),893        "Steg":      (0.10, min(steg_susp,      100)),894        "GAN":       (0.08, min(gan_cb,         100)),895        "FaceSeam":  (0.07, min(face_seam,      100)),896    }897    score = sum(wt*val for wt,val in w.values())898    return min(100.0, score), {k: v[1] for k,v in w.items()}899 900def verdict_from_score(s):901    if   s < 15: return "AUTHENTIC",          "#22c55e"902    elif s < 35: return "LIKELY AUTHENTIC",   "#84cc16"903    elif s < 55: return "INCONCLUSIVE",        "#facc15"904    elif s < 75: return "SUSPICIOUS",          "#f97316"905    else:        return "LIKELY MANIPULATED",  "#ef4444"906 907def radar_chart(scores: dict, label: str) -> go.Figure:908    cats = list(scores.keys())909    vals = [scores[k] for k in cats]910    vals_closed = vals + [vals[0]]911    cats_closed = cats + [cats[0]]912    fig = go.Figure()913    fig.add_trace(go.Scatterpolar(914        r=vals_closed, theta=cats_closed,915        fill="toself",916        fillcolor="rgba(0,255,156,0.15)",917        line=dict(color="#00ff9c", width=2),918        name=label,919    ))920    fig.update_layout(921        polar=dict(922            radialaxis=dict(visible=True, range=[0,100],923                            tickfont=dict(color="#9affd7",size=9),924                            gridcolor="rgba(0,255,156,0.2)"),925            angularaxis=dict(tickfont=dict(color="#00ff9c",size=10),926                             gridcolor="rgba(0,255,156,0.13)"),927            bgcolor="#000",928        ),929        paper_bgcolor="#000",930        plot_bgcolor="#000",931        showlegend=False,932        margin=dict(l=30,r=30,t=40,b=30),933        title=dict(text=label,font=dict(color="#00ff9c",size=13)),934    )935    return fig936 937 938# ══════════════════════════════════════════════════════════════════════939# ── MODULE 19 · CHAIN-OF-CUSTODY LOG ─────────────────────────────────940# ══════════════════════════════════════════════════════════════════════941class CustodyLog:942    def __init__(self):943        self.entries = []944 945    def add(self, action: str, detail: str = ""):946        self.entries.append({947            "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S.%f UTC"),948            "action":    action,949            "detail":    detail,950        })951 952    def to_text(self) -> str:953        lines = ["╔══ CHAIN OF CUSTODY LOG ══════════════════════════════════╗",954                 "║  Psychomods Cyber Forensic Lab v3.0                      ║",955                 "╚══════════════════════════════════════════════════════════╝",""]956        for e in self.entries:957            lines.append(f"[{e['timestamp']}]  {e['action']}")958            if e["detail"]:959                lines.append(f"    ↳ {e['detail']}")960        return "\n".join(lines)961 962    def to_json(self) -> str:963        return json.dumps(self.entries, indent=2)964 965 966# ══════════════════════════════════════════════════════════════════════967# ── MODULE 20 · PDF REPORT ────────────────────────────────────────────968# ══════════════════════════════════════════════════════════════════════969def build_pdf(label_a, label_b, info_a, info_b,970              stats_a, stats_b, anomalies_a, anomalies_b,971              custody_log: str) -> io.BytesIO:972    buf = io.BytesIO()973    doc = SimpleDocTemplate(buf, pagesize=A4,974                            leftMargin=18*mm, rightMargin=18*mm,975                            topMargin=18*mm, bottomMargin=18*mm)976    G   = rl_colors.HexColor("#007744")977    BK  = rl_colors.HexColor("#111")978    styles = getSampleStyleSheet()979    T  = ParagraphStyle("T",  fontName="Helvetica-Bold",   fontSize=20, textColor=G,   alignment=1, spaceAfter=4)980    S  = ParagraphStyle("S",  fontName="Helvetica",        fontSize=10, textColor=rl_colors.grey, alignment=1, spaceAfter=12)981    H2 = ParagraphStyle("H2", fontName="Helvetica-Bold",   fontSize=13, textColor=G,   spaceBefore=10, spaceAfter=4)982    BD = ParagraphStyle("BD", fontName="Helvetica",        fontSize=9,  textColor=BK,  spaceAfter=3,   leading=13)983    WN = ParagraphStyle("WN", fontName="Helvetica-Oblique",fontSize=9,  textColor=rl_colors.HexColor("#cc5500"), spaceAfter=3)984    OK = ParagraphStyle("OK", fontName="Helvetica-Oblique",fontSize=9,  textColor=rl_colors.HexColor("#007744"), spaceAfter=3)985 986    story = []987    story.append(Paragraph("🧠 PSYCHOMODS CYBER FORENSIC LAB", T))988    story.append(Paragraph("Advanced Image Manipulation Detection Report — v3.0", S))989    story.append(Paragraph(f"Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}", S))990    story.append(HRFlowable(width="100%", thickness=1, color=G))991    story.append(Spacer(1,8))992 993    def img_section(label, info, stats, anomalies):994        story.append(Paragraph(f"► {label}", H2))995        story.append(HRFlowable(width="60%", thickness=.5, color=G))996        story.append(Spacer(1,4))997        # Info table (skip if empty — single-image mode)998        if info:999            td = [[Paragraph(f"<b>{k}</b>",BD), Paragraph(v,BD)] for k,v in info.items()]1000            t  = Table(td, colWidths=[55*mm,115*mm])1001            t.setStyle(TableStyle([1002                ("BACKGROUND",(0,0),(0,-1),rl_colors.HexColor("#e8fff5")),1003                ("GRID",(0,0),(-1,-1),.3,rl_colors.HexColor("#aaddcc")),1004                ("ROWBACKGROUNDS",(0,0),(-1,-1),[rl_colors.HexColor("#f0fff8"),rl_colors.white]),1005            ])); story.append(t); story.append(Spacer(1,8))1006        # Stats table (skip if empty)1007        if stats:1008            story.append(Paragraph("Forensic Metrics", H2))1009            sd = [[Paragraph(f"<b>{k}</b>",BD), Paragraph(str(v),BD)] for k,v in stats.items()]1010            t2 = Table(sd, colWidths=[75*mm,95*mm])1011            t2.setStyle(TableStyle([1012                ("BACKGROUND",(0,0),(0,-1),rl_colors.HexColor("#e8fff5")),1013                ("GRID",(0,0),(-1,-1),.3,rl_colors.HexColor("#aaddcc")),1014                ("ROWBACKGROUNDS",(0,0),(-1,-1),[rl_colors.HexColor("#f0fff8"),rl_colors.white]),1015            ])); story.append(t2); story.append(Spacer(1,8))1016        # Anomalies1017        if anomalies:1018            story.append(Paragraph("⚠️ Anomalies Detected", H2))1019            for a in anomalies:1020                story.append(Paragraph(f"• {a}", WN))1021        else:1022            story.append(Paragraph("✓ No anomalies detected", OK))1023        story.append(Spacer(1,12))1024 1025    img_section(label_a, info_a, stats_a, anomalies_a)1026    img_section(label_b, info_b, stats_b, anomalies_b)1027 1028    # Custody log (truncated)1029    story.append(HRFlowable(width="100%",thickness=1,color=G))1030    story.append(Paragraph("Chain of Custody Log", H2))1031    for line in custody_log.split("\n")[:40]:1032        story.append(Paragraph(line or " ", BD))1033 1034    story.append(Spacer(1,8))1035    story.append(HRFlowable(width="100%",thickness=.5,color=G))1036    story.append(Paragraph(1037        "DISCLAIMER: Algorithmic results only. Validate with a certified examiner before legal use.",1038        ParagraphStyle("disc",fontName="Helvetica-Oblique",fontSize=8,textColor=rl_colors.grey)))1039    story.append(Paragraph(1040        "Psychomods Cyber Security Tools | https://psychomods.infy.uk",1041        ParagraphStyle("foot",fontName="Helvetica",fontSize=8,textColor=G,alignment=1)))1042 1043    doc.build(story); buf.seek(0)1044    return buf1045 1046 1047# ══════════════════════════════════════════════════════════════════════1048# ── MODULE 21 · ZIP EVIDENCE PACKAGE ─────────────────────────────────1049# ══════════════════════════════════════════════════════════════════════1050def build_zip(pdf_buf, txt_report, custody_json, images: dict) -> io.BytesIO:1051    zb = io.BytesIO()1052    with zipfile.ZipFile(zb, "w", zipfile.ZIP_DEFLATED) as zf:1053        zf.writestr("forensic_report.pdf",  pdf_buf.getvalue())1054        zf.writestr("forensic_report.txt",  txt_report)1055        zf.writestr("chain_of_custody.json",custody_json)1056        for name, pil_img in images.items():1057            ib = io.BytesIO()1058            pil_img.save(ib,"PNG"); ib.seek(0)1059            zf.writestr(f"analysis_images/{name}.png", ib.getvalue())1060    zb.seek(0); return zb1061 1062 1063# ══════════════════════════════════════════════════════════════════════1064# SIDEBAR1065# ══════════════════════════════════════════════════════════════════════1066with st.sidebar:1067    st.markdown("### ⚙️ Settings")1068    mode = st.radio("Analysis Mode",1069                    ["🔁 Two-Image Compare","📷 Single Image","📦 Batch (up to 10)"],1070                    index=0)1071    st.markdown("---")1072    ela_q   = st.slider("ELA JPEG Quality",   60, 98, 92)1073    ela_s   = st.slider("ELA Amplification",   5, 40, 20)1074    ela_t   = st.slider("Region Threshold",    5, 80, 20)1075    min_a   = st.slider("Min Region Area px", 50,2000,300,50)1076    diff_amp= st.slider("Pixel Diff Amplify",  1, 20,  5)1077    st.markdown("---")1078    st.markdown("""1079**v3.0 Modules**10801. ELA + Heatmap10812. Region Detection10823. Clone/Copy-Move10834. Noise Residual10845. Edge Analysis10856. FFT Spectrum10867. DCT Block Analysis10878. LBP Texture10889. JPEG Ghost108910. Luminance Gradient109011. Histogram109112. **Steganography (LSB)**109213. **Metadata Timeline**109314. **Splicing Boundary**109415. **GAN/AI Artifact**109516. **Face Analysis**109617. **Pixel Diff**109718. **Composite + Radar**109819. **Chain of Custody**109920. **ZIP Evidence Export**1100 1101[🌐 psychomods.infy.uk](https://psychomods.infy.uk)1102""")1103 1104 1105# ══════════════════════════════════════════════════════════════════════1106# HELPER: run full pipeline on a single image1107# ══════════════════════════════════════════════════════════════════════1108def run_pipeline(img: Image.Image, label: str, uf=None,1109                 log: CustodyLog = None, prog=None, base=0, span=100):1110 1111    def tick(pct, msg):1112        if prog: prog.progress(base + int(pct*span/100), msg)1113        if log:  log.add(msg, label)1114 1115    results = {}1116    imgs    = {}1117 1118    tick(2,  "ELA…")1119    e   = ela(img, ela_q, ela_s)1120    em  = ela_heatmap(e)1121    er, n_regions = ela_region_detect(img, e, ela_t, min_a)1122    mean,std,p95,prob,verdict,color = ela_stats(e)1123    results["ela"]    = dict(mean=mean,std=std,p95=p95,prob=prob,1124                              verdict=verdict,color=color,n_regions=n_regions)1125    imgs["ela"] = e; imgs["ela_heat"] = em; imgs["ela_regions"] = er1126 1127    tick(12, "Clone detection…")1128    cl_img, cp = clone_detect(img)1129    results["clone"] = dict(pairs=cp)1130    imgs["clone"] = cl_img1131 1132    tick(22, "Noise residual…")1133    nr_img, sig = noise_residual(img)1134    results["noise"] = dict(sigma=sig)1135    imgs["noise"] = nr_img1136 1137    tick(30, "Edge analysis…")1138    ed_img, dens = edge_analysis(img)1139    results["edge"] = dict(density=dens)1140    imgs["edge"] = ed_img1141 1142    tick(36, "FFT…")1143    imgs["fft"] = fft_analysis(img)1144 1145    tick(42, "DCT…")1146    dct_img, dct_s = dct_analysis(img)1147    results["dct"] = dict(score=dct_s)1148    imgs["dct"] = dct_img1149 1150    tick(48, "LBP texture…")1151    imgs["lbp"] = lbp_analysis(img)1152 1153    tick(52, "JPEG ghost…")1154    imgs["jpeg_ghost"] = jpeg_ghost(img)1155 1156    tick(56, "Luminance gradient…")1157    imgs["lum_grad"] = luminance_gradient(img)1158 1159    tick(60, "Histogram…")1160    imgs["histogram"] = histogram_plot(img)1161 1162    tick(64, "Steganography…")1163    steg_lsb = steg_lsb_planes(img)1164    steg_ent = steg_entropy_map(img)1165    steg_bp  = steg_bit_planes(img)1166    chi2, steg_susp, steg_flag = steg_chi_square(img)1167    results["steg"] = dict(chi2=chi2,susp=steg_susp,flag=steg_flag)1168    imgs["steg_lsb"] = steg_lsb; imgs["steg_entropy"] = steg_ent1169    imgs["steg_bitplanes"] = steg_bp1170 1171    tick(70, "Metadata timeline…")1172    exif_data    = extract_exif_full(img)1173    events, anomalies, meta_risk = metadata_timeline(exif_data)1174    results["meta"] = dict(events=events,anomalies=anomalies,risk=meta_risk)1175 1176    tick(76, "Splicing boundary…")1177    spl_img, spl_s = splicing_boundary(img)1178    results["splice"] = dict(score=spl_s)1179    imgs["splicing"] = spl_img1180 1181    tick(82, "GAN/AI artifact detection…")1182    gan_fft_img, cb_score, cb_peaks = gan_checkerboard(img)1183    gan_tex_img, tex_score          = gan_texture_variance(img)1184    gan_col_img, col_score, ch_corr = gan_color_inconsistency(img)1185    gan_pow_img, pow_score          = gan_frequency_fingerprint(img)1186    gan_score = (cb_score*0.35 + tex_score*0.25 +1187                 col_score*0.20 + (pow_score or 0)*0.20)1188    results["gan"] = dict(cb=cb_score,texture=tex_score,1189                           color=col_score,power=pow_score or 0,1190                           score=gan_score,corr=ch_corr)1191    imgs["gan_fft"] = gan_fft_img; imgs["gan_tex"] = gan_tex_img1192    imgs["gan_col"] = gan_col_img1193    if gan_pow_img: imgs["gan_pow"] = gan_pow_img1194 1195    tick(90, "Face analysis…")1196    face_img, face_res     = face_analysis_cv(img)1197    seam_img, seam_score   = face_blending_boundary(img)1198    results["face"]  = dict(data=face_res, seam=seam_score)1199    imgs["face"] = face_img; imgs["face_seam"] = seam_img1200 

Showing the first 1,200 of 1656 lines. Download the file for the rest.