Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -5,7 +5,7 @@ import cv2
|
|
| 5 |
from scipy import ndimage
|
| 6 |
|
| 7 |
# ═══════════════════════════════════════════════════
|
| 8 |
-
# 🛡️ MEDIASHIELD PRO v2.
|
| 9 |
# ═══════════════════════════════════════════════════
|
| 10 |
|
| 11 |
def estimate_sensor_noise(img):
|
|
@@ -16,74 +16,66 @@ def estimate_sensor_noise(img):
|
|
| 16 |
return np.std(noise)
|
| 17 |
|
| 18 |
def analyze_local_variance(img):
|
| 19 |
-
"""Compare le centre
|
| 20 |
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY).astype(np.float32)
|
| 21 |
h, w = gray.shape
|
| 22 |
-
# Zone centrale vs Zone bordure
|
| 23 |
center = gray[h//4:3*h//4, w//4:3*w//4]
|
| 24 |
edge = gray[0:h//4, 0:w//4]
|
| 25 |
-
|
| 26 |
def get_var(z):
|
| 27 |
return np.var(z - cv2.GaussianBlur(z, (5,5), 0))
|
| 28 |
-
|
| 29 |
v_c, v_e = get_var(center), get_var(edge)
|
| 30 |
return abs(v_c - v_e) / (v_e + 1e-8)
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
def analyze_chrominance_noise(img):
|
| 33 |
ycrcb = cv2.cvtColor(img, cv2.COLOR_RGB2YCrCb)
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
cr_var = ndimage.generic_filter(cr, np.var, size=5)
|
| 37 |
-
cb_var = ndimage.generic_filter(cb, np.var, size=5)
|
| 38 |
u_cr = np.std(cr_var) / (np.mean(cr_var) + 1e-8)
|
| 39 |
u_cb = np.std(cb_var) / (np.mean(cb_var) + 1e-8)
|
| 40 |
-
return (u_cr + u_cb) / 2
|
| 41 |
|
| 42 |
def detect_grid_and_ringing(img):
|
| 43 |
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY).astype(np.float32)
|
| 44 |
-
|
| 45 |
-
fshift = np.fft.fftshift(f)
|
| 46 |
magnitude = np.abs(fshift)
|
| 47 |
rows, cols = gray.shape
|
| 48 |
crow, ccol = rows//2, cols//2
|
| 49 |
magnitude[(np.ogrid[:rows,:cols][0]-crow)**2 + (np.ogrid[:rows,:cols][1]-ccol)**2 <= (min(rows,cols)//10)**2] = 0
|
| 50 |
-
|
| 51 |
def get_peaks(sig):
|
| 52 |
thresh = np.max(sig) * 0.25
|
| 53 |
-
return [i for i in range(1, len(sig)-1) if sig[i] > thresh and sig[i] > sig[i-1] and sig[i] > sig[i+1] and i%8 != 0]
|
| 54 |
-
|
| 55 |
grid_s = (len(get_peaks(np.sum(magnitude, 0))) + len(get_peaks(np.sum(magnitude, 1)))) / 2
|
| 56 |
ringing = np.mean(magnitude) / (np.max(magnitude) + 1e-8)
|
| 57 |
fft_vis = cv2.applyColorMap(cv2.normalize(np.log(magnitude+1), None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8), cv2.COLORMAP_VIRIDIS)
|
| 58 |
return grid_s, ringing, fft_vis
|
| 59 |
|
| 60 |
def error_level_analysis(img, quality=90):
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
diff = cv2.absdiff(img, dec_rgb)
|
| 65 |
return np.mean(diff), cv2.normalize(diff, None, 0, 255, cv2.NORM_MINMAX)
|
| 66 |
|
| 67 |
def compute_ai_score_v4(metrics):
|
| 68 |
-
score = 0
|
| 69 |
-
reasons = []
|
| 70 |
-
|
| 71 |
-
# 1. Signatures IA classiques
|
| 72 |
if metrics['grid_score'] > 25: score += 30; reasons.append("⚠️ Motifs réguliers (GAN possible)")
|
| 73 |
if metrics['ringing'] > 0.3: score += 25; reasons.append("⚠️ Artefacts circulaires")
|
| 74 |
-
|
| 75 |
-
# 2. Analyse du grain (Bruit Capteur)
|
| 76 |
if metrics['sensor_noise'] < 1.5: score += 25; reasons.append("⚡ Image trop lisse (IA probable)")
|
| 77 |
elif metrics['sensor_noise'] > 4.0: score -= 25; reasons.append("✅ Bruit naturel (Photo réelle)")
|
| 78 |
-
|
| 79 |
-
# 3. Analyse Locale (Détection de montage)
|
| 80 |
-
if metrics['local_diff'] > 1.8: score += 30; reasons.append("🚨 Incohérence de texture (IA locale / Montage)")
|
| 81 |
-
|
| 82 |
-
# 4. Anti Faux-Positifs (Filtre compression réseaux sociaux)
|
| 83 |
if metrics['ela_score'] > 2.5 and metrics['grid_score'] < 8:
|
| 84 |
-
score *= 0.3
|
| 85 |
-
reasons.append("✅ Structure typique d'une photo réelle compressée")
|
| 86 |
-
|
| 87 |
return int(max(0, min(score, 100))), reasons
|
| 88 |
|
| 89 |
def analyze_image(img_input):
|
|
@@ -94,36 +86,48 @@ def analyze_image(img_input):
|
|
| 94 |
mean_b = np.mean(cv2.cvtColor(img, cv2.COLOR_RGB2GRAY))
|
| 95 |
img_analysed = cv2.LUT(img, np.array([((i/255.0)**(1/0.6))*255 for i in range(256)]).astype("uint8")) if mean_b < 60 else img.copy()
|
| 96 |
|
| 97 |
-
#
|
| 98 |
-
chrom_u
|
| 99 |
grid_s, ring, fft_v = detect_grid_and_ringing(img_analysed)
|
| 100 |
ela_s, ela_v = error_level_analysis(img_analysed)
|
| 101 |
sensor_n = estimate_sensor_noise(img_analysed)
|
| 102 |
local_d = analyze_local_variance(img_analysed)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
metrics = {'chrom_uniformity': chrom_u, 'grid_score': grid_s, 'ringing': ring, 'ela_score': ela_s, 'sensor_noise': sensor_n, 'local_diff': local_d}
|
| 105 |
ai_score, reasons = compute_ai_score_v4(metrics)
|
| 106 |
|
| 107 |
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
|
| 108 |
-
axes[0,0].imshow(
|
| 109 |
axes[0,1].imshow(fft_v); axes[0,1].set_title('Fréquences (FFT)')
|
| 110 |
axes[1,0].imshow(ela_v); axes[1,0].set_title('ELA (Compression)')
|
| 111 |
-
axes[1,1].imshow(
|
| 112 |
for ax in axes.flatten(): ax.axis('off')
|
| 113 |
plt.tight_layout()
|
| 114 |
|
| 115 |
-
report = f"🛡️ MEDIASHIELD v2.
|
| 116 |
return fig, report
|
| 117 |
|
| 118 |
with gr.Blocks() as demo:
|
| 119 |
-
gr.Markdown("# 🛡️ MediaShield PRO v2.
|
| 120 |
with gr.Row():
|
| 121 |
with gr.Column():
|
| 122 |
in_img = gr.Image(type="numpy")
|
| 123 |
-
btn = gr.Button("
|
| 124 |
with gr.Column():
|
| 125 |
out_plt = gr.Plot()
|
| 126 |
-
out_txt = gr.Textbox(label="
|
| 127 |
btn.click(analyze_image, in_img, [out_plt, out_txt])
|
| 128 |
|
| 129 |
if __name__ == "__main__":
|
|
|
|
| 5 |
from scipy import ndimage
|
| 6 |
|
| 7 |
# ═══════════════════════════════════════════════════
|
| 8 |
+
# 🛡️ MEDIASHIELD PRO v2.5 – Localized AI Detection
|
| 9 |
# ═══════════════════════════════════════════════════
|
| 10 |
|
| 11 |
def estimate_sensor_noise(img):
|
|
|
|
| 16 |
return np.std(noise)
|
| 17 |
|
| 18 |
def analyze_local_variance(img):
|
| 19 |
+
"""Compare le centre aux bords pour détecter un montage IA."""
|
| 20 |
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY).astype(np.float32)
|
| 21 |
h, w = gray.shape
|
|
|
|
| 22 |
center = gray[h//4:3*h//4, w//4:3*w//4]
|
| 23 |
edge = gray[0:h//4, 0:w//4]
|
|
|
|
| 24 |
def get_var(z):
|
| 25 |
return np.var(z - cv2.GaussianBlur(z, (5,5), 0))
|
|
|
|
| 26 |
v_c, v_e = get_var(center), get_var(edge)
|
| 27 |
return abs(v_c - v_e) / (v_e + 1e-8)
|
| 28 |
|
| 29 |
+
def compute_noise_incoherence_map(img):
|
| 30 |
+
"""Génère une carte thermique où l'IA lissée apparaît en rouge brillant."""
|
| 31 |
+
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY).astype(np.float32)
|
| 32 |
+
# Isoler le grain
|
| 33 |
+
grain = gray - cv2.medianBlur(gray, 7)
|
| 34 |
+
# Calculer la variance locale (IA = faible variance)
|
| 35 |
+
grain_var = ndimage.generic_filter(grain, np.var, size=15)
|
| 36 |
+
# Inverser pour que l'IA soit "chaude" (brillante)
|
| 37 |
+
incoherence_map = np.max(grain_var) - grain_var
|
| 38 |
+
vis = cv2.normalize(incoherence_map, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
|
| 39 |
+
return cv2.applyColorMap(vis, cv2.COLORMAP_JET)
|
| 40 |
+
|
| 41 |
def analyze_chrominance_noise(img):
|
| 42 |
ycrcb = cv2.cvtColor(img, cv2.COLOR_RGB2YCrCb)
|
| 43 |
+
cr_var = ndimage.generic_filter(ycrcb[:,:,1].astype(np.float32), np.var, size=5)
|
| 44 |
+
cb_var = ndimage.generic_filter(ycrcb[:,:,2].astype(np.float32), np.var, size=5)
|
|
|
|
|
|
|
| 45 |
u_cr = np.std(cr_var) / (np.mean(cr_var) + 1e-8)
|
| 46 |
u_cb = np.std(cb_var) / (np.mean(cb_var) + 1e-8)
|
| 47 |
+
return (u_cr + u_cb) / 2
|
| 48 |
|
| 49 |
def detect_grid_and_ringing(img):
|
| 50 |
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY).astype(np.float32)
|
| 51 |
+
fshift = np.fft.fftshift(np.fft.fft2(gray))
|
|
|
|
| 52 |
magnitude = np.abs(fshift)
|
| 53 |
rows, cols = gray.shape
|
| 54 |
crow, ccol = rows//2, cols//2
|
| 55 |
magnitude[(np.ogrid[:rows,:cols][0]-crow)**2 + (np.ogrid[:rows,:cols][1]-ccol)**2 <= (min(rows,cols)//10)**2] = 0
|
|
|
|
| 56 |
def get_peaks(sig):
|
| 57 |
thresh = np.max(sig) * 0.25
|
| 58 |
+
return [i for i in range(1, len(sig)-1) if sig[i] > thresh and sig[i] > sig[i-1] and sig[i] > sig[i+1] and i%8 != 0]
|
|
|
|
| 59 |
grid_s = (len(get_peaks(np.sum(magnitude, 0))) + len(get_peaks(np.sum(magnitude, 1)))) / 2
|
| 60 |
ringing = np.mean(magnitude) / (np.max(magnitude) + 1e-8)
|
| 61 |
fft_vis = cv2.applyColorMap(cv2.normalize(np.log(magnitude+1), None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8), cv2.COLORMAP_VIRIDIS)
|
| 62 |
return grid_s, ringing, fft_vis
|
| 63 |
|
| 64 |
def error_level_analysis(img, quality=90):
|
| 65 |
+
_, enc = cv2.imencode('.jpg', cv2.cvtColor(img, cv2.COLOR_RGB2BGR), [int(cv2.IMWRITE_JPEG_QUALITY), quality])
|
| 66 |
+
dec = cv2.cvtColor(cv2.imdecode(enc, 1), cv2.COLOR_BGR2RGB)
|
| 67 |
+
diff = cv2.absdiff(img, dec)
|
|
|
|
| 68 |
return np.mean(diff), cv2.normalize(diff, None, 0, 255, cv2.NORM_MINMAX)
|
| 69 |
|
| 70 |
def compute_ai_score_v4(metrics):
|
| 71 |
+
score, reasons = 0, []
|
|
|
|
|
|
|
|
|
|
| 72 |
if metrics['grid_score'] > 25: score += 30; reasons.append("⚠️ Motifs réguliers (GAN possible)")
|
| 73 |
if metrics['ringing'] > 0.3: score += 25; reasons.append("⚠️ Artefacts circulaires")
|
|
|
|
|
|
|
| 74 |
if metrics['sensor_noise'] < 1.5: score += 25; reasons.append("⚡ Image trop lisse (IA probable)")
|
| 75 |
elif metrics['sensor_noise'] > 4.0: score -= 25; reasons.append("✅ Bruit naturel (Photo réelle)")
|
| 76 |
+
if metrics['local_diff'] > 1.6: score += 35; reasons.append("🚨 Montage local détecté (Incohérence)")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
if metrics['ela_score'] > 2.5 and metrics['grid_score'] < 8:
|
| 78 |
+
score *= 0.3; reasons.append("✅ Structure cohérente avec compression réelle")
|
|
|
|
|
|
|
| 79 |
return int(max(0, min(score, 100))), reasons
|
| 80 |
|
| 81 |
def analyze_image(img_input):
|
|
|
|
| 86 |
mean_b = np.mean(cv2.cvtColor(img, cv2.COLOR_RGB2GRAY))
|
| 87 |
img_analysed = cv2.LUT(img, np.array([((i/255.0)**(1/0.6))*255 for i in range(256)]).astype("uint8")) if mean_b < 60 else img.copy()
|
| 88 |
|
| 89 |
+
# Analyses
|
| 90 |
+
chrom_u = analyze_chrominance_noise(img_analysed)
|
| 91 |
grid_s, ring, fft_v = detect_grid_and_ringing(img_analysed)
|
| 92 |
ela_s, ela_v = error_level_analysis(img_analysed)
|
| 93 |
sensor_n = estimate_sensor_noise(img_analysed)
|
| 94 |
local_d = analyze_local_variance(img_analysed)
|
| 95 |
+
noise_map = compute_noise_incoherence_map(img_analysed)
|
| 96 |
+
|
| 97 |
+
# Détection de la zone suspecte et dessin du rectangle
|
| 98 |
+
img_boxed = img.copy()
|
| 99 |
+
if local_d > 1.3: # Seuil de détection de montage
|
| 100 |
+
gray_map = cv2.cvtColor(noise_map, cv2.COLOR_BGR2GRAY)
|
| 101 |
+
_, _, _, max_loc = cv2.minMaxLoc(gray_map)
|
| 102 |
+
h, w = img.shape[:2]
|
| 103 |
+
size = int(min(h, w) * 0.25)
|
| 104 |
+
top_left = (max(0, max_loc[0] - size//2), max(0, max_loc[1] - size//2))
|
| 105 |
+
bottom_right = (min(w, max_loc[0] + size//2), min(h, max_loc[1] + size//2))
|
| 106 |
+
cv2.rectangle(img_boxed, top_left, bottom_right, (255, 0, 0), 4)
|
| 107 |
|
| 108 |
metrics = {'chrom_uniformity': chrom_u, 'grid_score': grid_s, 'ringing': ring, 'ela_score': ela_s, 'sensor_noise': sensor_n, 'local_diff': local_d}
|
| 109 |
ai_score, reasons = compute_ai_score_v4(metrics)
|
| 110 |
|
| 111 |
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
|
| 112 |
+
axes[0,0].imshow(img_boxed); axes[0,0].set_title('Détection Locale (IA)')
|
| 113 |
axes[0,1].imshow(fft_v); axes[0,1].set_title('Fréquences (FFT)')
|
| 114 |
axes[1,0].imshow(ela_v); axes[1,0].set_title('ELA (Compression)')
|
| 115 |
+
axes[1,1].imshow(noise_map); axes[1,1].set_title('Heatmap Incohérence')
|
| 116 |
for ax in axes.flatten(): ax.axis('off')
|
| 117 |
plt.tight_layout()
|
| 118 |
|
| 119 |
+
report = f"🛡️ MEDIASHIELD v2.5\nScore AI : {ai_score}/100\n\n" + ("\n".join(reasons) if reasons else "✅ Authentique")
|
| 120 |
return fig, report
|
| 121 |
|
| 122 |
with gr.Blocks() as demo:
|
| 123 |
+
gr.Markdown("# 🛡️ MediaShield PRO v2.5\nSami Meddeb - ACoNum")
|
| 124 |
with gr.Row():
|
| 125 |
with gr.Column():
|
| 126 |
in_img = gr.Image(type="numpy")
|
| 127 |
+
btn = gr.Button("DÉMARRER L'ANALYSE", variant="primary")
|
| 128 |
with gr.Column():
|
| 129 |
out_plt = gr.Plot()
|
| 130 |
+
out_txt = gr.Textbox(label="Résultat Forensic", lines=10)
|
| 131 |
btn.click(analyze_image, in_img, [out_plt, out_txt])
|
| 132 |
|
| 133 |
if __name__ == "__main__":
|