abinazebinoy commited on
Commit
e062f0b
·
1 Parent(s): 5928294

feat:ELA (Error Level Analysis) signal (23rd signal)

Browse files

ELA detects JPEG compression inconsistencies across image regions.
Authentic photos have consistent compression history.
AI-generated images show uniform low error (synthesized uniformly).
Manipulated images show regional inconsistencies.

- ela_detector.py: JPEG re-compression difference analysis
* Global error level analysis
* Regional block variance (coefficient of variation)
* High-error concentration analysis
- ensemble: ELA wired in, weights updated, v1.2
- 23 total signals (was 22)
- Tests updated to 23

backend/services/advanced_ensemble_detector.py CHANGED
@@ -8,6 +8,7 @@ from backend.services.statistical_detector import StatisticalDetector
8
  from backend.services.dire_detector import DIREDetector
9
  from backend.services.clip_detector import CLIPDetector
10
  from backend.services.prnu_detector import detect_prnu
 
11
 
12
  logger = setup_logger(__name__)
13
 
@@ -37,7 +38,7 @@ class AdvancedEnsembleDetector(StatisticalDetector):
37
  Run complete advanced detection with all methods.
38
 
39
  Returns:
40
- Complete report with 22 detection signals
41
  """
42
  logger.info(f"Starting advanced ensemble detection for {self.filename}")
43
 
@@ -53,8 +54,11 @@ class AdvancedEnsembleDetector(StatisticalDetector):
53
  # Add PRNU signal
54
  prnu_result = detect_prnu(self.image_bytes, self.filename)
55
 
56
- # Combine all signals (now 22 total)
57
- all_signals = base_report["all_signals"] + [dire_result, clip_result, prnu_result]
 
 
 
58
 
59
  # Recalculate final score with weighted ensemble
60
  # Weights based on validation performance
@@ -62,33 +66,26 @@ class AdvancedEnsembleDetector(StatisticalDetector):
62
 
63
  prnu_confidence = prnu_result.get("confidence", 0.0)
64
 
65
- if dire_confidence > 0.0 and prnu_confidence > 0.0:
 
 
 
66
  weighted_score = (
67
- 0.38 * base_report["ai_probability"] +
68
- 0.30 * dire_result["score"] +
69
- 0.22 * clip_result["score"] +
70
- 0.10 * prnu_result["score"]
 
71
  )
72
- elif dire_confidence > 0.0:
 
 
73
  weighted_score = (
74
- 0.40 * base_report["ai_probability"] +
75
- 0.35 * dire_result["score"] +
76
- 0.25 * clip_result["score"]
 
77
  )
78
- else:
79
- # DIRE unavailable — use statistical+CLIP+PRNU
80
- logger.info("DIRE unavailable — using statistical+CLIP+PRNU")
81
- if prnu_confidence > 0.0:
82
- weighted_score = (
83
- 0.58 * base_report["ai_probability"] +
84
- 0.30 * clip_result["score"] +
85
- 0.12 * prnu_result["score"]
86
- )
87
- else:
88
- weighted_score = (
89
- 0.65 * base_report["ai_probability"] +
90
- 0.35 * clip_result["score"]
91
- )
92
 
93
  suspicious_count = sum(1 for s in all_signals if s["score"] > 0.5)
94
 
@@ -130,8 +127,8 @@ class AdvancedEnsembleDetector(StatisticalDetector):
130
  "summary": f"Analyzed using {len(all_signals)} independent signals including "
131
  f"statistical analysis, diffusion reconstruction, and semantic embeddings. "
132
  f"{suspicious_count} signals indicate AI generation.",
133
- "detection_version": "advanced-ensemble-v1.1",
134
- "methods_used": ["statistical", "dire", "clip", "prnu"]
135
  }
136
 
137
  logger.info(
 
8
  from backend.services.dire_detector import DIREDetector
9
  from backend.services.clip_detector import CLIPDetector
10
  from backend.services.prnu_detector import detect_prnu
11
+ from backend.services.ela_detector import detect_ela
12
 
13
  logger = setup_logger(__name__)
14
 
 
38
  Run complete advanced detection with all methods.
39
 
40
  Returns:
41
+ Complete report with 23 detection signals
42
  """
43
  logger.info(f"Starting advanced ensemble detection for {self.filename}")
44
 
 
54
  # Add PRNU signal
55
  prnu_result = detect_prnu(self.image_bytes, self.filename)
56
 
57
+ # Add ELA signal
58
+ ela_result = detect_ela(self.image_bytes, self.filename)
59
+
60
+ # Combine all signals (now 23 total)
61
+ all_signals = base_report["all_signals"] + [dire_result, clip_result, prnu_result, ela_result]
62
 
63
  # Recalculate final score with weighted ensemble
64
  # Weights based on validation performance
 
66
 
67
  prnu_confidence = prnu_result.get("confidence", 0.0)
68
 
69
+ ela_confidence = ela_result.get("confidence", 0.0)
70
+ prnu_confidence = prnu_result.get("confidence", 0.0)
71
+
72
+ if dire_confidence > 0.0:
73
  weighted_score = (
74
+ 0.35 * base_report["ai_probability"] +
75
+ 0.28 * dire_result["score"] +
76
+ 0.20 * clip_result["score"] +
77
+ 0.10 * prnu_result["score"] +
78
+ 0.07 * ela_result["score"]
79
  )
80
+ else:
81
+ # DIRE unavailable
82
+ logger.info("DIRE unavailable — using statistical+CLIP+PRNU+ELA")
83
  weighted_score = (
84
+ 0.55 * base_report["ai_probability"] +
85
+ 0.25 * clip_result["score"] +
86
+ 0.12 * prnu_result["score"] +
87
+ 0.08 * ela_result["score"]
88
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
  suspicious_count = sum(1 for s in all_signals if s["score"] > 0.5)
91
 
 
127
  "summary": f"Analyzed using {len(all_signals)} independent signals including "
128
  f"statistical analysis, diffusion reconstruction, and semantic embeddings. "
129
  f"{suspicious_count} signals indicate AI generation.",
130
+ "detection_version": "advanced-ensemble-v1.2",
131
+ "methods_used": ["statistical", "dire", "clip", "prnu", "ela"]
132
  }
133
 
134
  logger.info(
backend/services/ela_detector.py ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ELA (Error Level Analysis) Detection.
3
+
4
+ ELA detects inconsistencies in JPEG compression across image regions.
5
+ When an image is authentic, all regions have similar compression error levels.
6
+ When an image is AI-generated or manipulated, regions show inconsistent
7
+ error levels because they have different compression histories.
8
+
9
+ Widely used in digital forensics, journalism verification, and court cases.
10
+ """
11
+ import numpy as np
12
+ from typing import Dict, Any
13
+ from PIL import Image, ImageChops, ImageEnhance
14
+ from io import BytesIO
15
+ from backend.core.logger import setup_logger
16
+
17
+ logger = setup_logger(__name__)
18
+
19
+
20
+ def detect_ela(image_bytes: bytes, filename: str = "unknown") -> Dict[str, Any]:
21
+ """
22
+ Perform Error Level Analysis on image.
23
+
24
+ Process:
25
+ 1. Re-save image at known JPEG quality (95)
26
+ 2. Compute pixel difference between original and re-saved
27
+ 3. Analyze the distribution of error levels across regions
28
+ 4. Inconsistent errors = manipulation or AI generation indicators
29
+ """
30
+ try:
31
+ # Open original image
32
+ original = Image.open(BytesIO(image_bytes)).convert("RGB")
33
+ width, height = original.size
34
+
35
+ # Skip tiny images
36
+ if width < 32 or height < 32:
37
+ return {
38
+ "signal_name": "ELA Compression Analysis",
39
+ "score": 0.5,
40
+ "confidence": 0.0,
41
+ "explanation": "Image too small for ELA analysis",
42
+ "method": "ela"
43
+ }
44
+
45
+ # Re-save at known quality
46
+ buffer = BytesIO()
47
+ original.save(buffer, format="JPEG", quality=95)
48
+ buffer.seek(0)
49
+ recompressed = Image.open(buffer).convert("RGB")
50
+
51
+ # Compute difference
52
+ diff = ImageChops.difference(original, recompressed)
53
+ diff_array = np.array(diff, dtype=np.float64)
54
+
55
+ # === Signal 1: Global error level ===
56
+ # AI images: very uniform low error (synthesized at consistent quality)
57
+ # Real photos: moderate variation in error levels
58
+ mean_error = float(np.mean(diff_array))
59
+ std_error = float(np.std(diff_array))
60
+
61
+ # === Signal 2: Regional inconsistency ===
62
+ # Divide image into blocks and measure error variance between blocks
63
+ block_size = max(16, min(width, height) // 8)
64
+ block_means = []
65
+
66
+ for y in range(0, height - block_size, block_size):
67
+ for x in range(0, width - block_size, block_size):
68
+ block = diff_array[y:y+block_size, x:x+block_size]
69
+ block_means.append(float(np.mean(block)))
70
+
71
+ if len(block_means) > 4:
72
+ block_variance = float(np.var(block_means))
73
+ block_mean = float(np.mean(block_means))
74
+ # Coefficient of variation: how inconsistent are regions?
75
+ cv = float(np.std(block_means) / (block_mean + 1e-10))
76
+ else:
77
+ block_variance = 0.0
78
+ cv = 0.0
79
+
80
+ # === Signal 3: High error region concentration ===
81
+ # AI images: error concentrated in specific patterns (e.g. edges)
82
+ # Real photos: error distributed across image
83
+ flat = diff_array.flatten()
84
+ high_error_pct = float(np.sum(flat > np.percentile(flat, 90)) / len(flat))
85
+ error_concentration = abs(high_error_pct - 0.10) # Expected ~10% above 90th pct
86
+
87
+ # === Combine into AI score ===
88
+ # Very low mean error + low variance = likely AI (uniform synthesis)
89
+ # Very high variance = likely manipulation
90
+
91
+ # Normalize mean error (real photos: typically 3-15, AI: 0.5-5)
92
+ if mean_error < 1.5:
93
+ mean_score = 0.8 # Very low error = AI signature
94
+ elif mean_error < 4.0:
95
+ mean_score = 0.5
96
+ elif mean_error < 10.0:
97
+ mean_score = 0.3 # Normal photo range
98
+ else:
99
+ mean_score = 0.4 # High error = possibly edited
100
+
101
+ # High coefficient of variation = inconsistent regions = manipulation
102
+ if cv > 2.0:
103
+ cv_score = 0.75 # Very inconsistent = manipulation
104
+ elif cv > 1.0:
105
+ cv_score = 0.55
106
+ else:
107
+ cv_score = 0.25 # Consistent = real or clean AI
108
+
109
+ # Error concentration anomaly
110
+ concentration_score = min(1.0, error_concentration * 5)
111
+
112
+ # Weighted combination
113
+ ai_score = (
114
+ 0.50 * mean_score +
115
+ 0.30 * cv_score +
116
+ 0.20 * concentration_score
117
+ )
118
+ ai_score = float(np.clip(ai_score, 0.0, 1.0))
119
+
120
+ # Confidence based on image size
121
+ pixel_count = width * height
122
+ confidence = min(0.80, 0.4 + (pixel_count / (512 * 512)) * 0.40)
123
+
124
+ if mean_error < 2.0:
125
+ explanation = (
126
+ f"Very low ELA error ({mean_error:.2f}) — "
127
+ "uniform compression consistent with AI synthesis"
128
+ )
129
+ elif cv > 1.5:
130
+ explanation = (
131
+ f"High regional ELA inconsistency (CV={cv:.2f}) — "
132
+ "compression anomalies detected across image regions"
133
+ )
134
+ else:
135
+ explanation = (
136
+ f"Normal ELA pattern (mean={mean_error:.2f}, CV={cv:.2f}) — "
137
+ "compression levels consistent with authentic photo"
138
+ )
139
+
140
+ logger.info(
141
+ f"ELA detection: score={ai_score:.3f}, "
142
+ f"mean_err={mean_error:.2f}, cv={cv:.2f}, file={filename}"
143
+ )
144
+
145
+ return {
146
+ "signal_name": "ELA Compression Analysis",
147
+ "score": ai_score,
148
+ "confidence": confidence,
149
+ "explanation": explanation,
150
+ "raw_value": mean_error,
151
+ "expected_range": "< 2.0 mean error for AI images",
152
+ "method": "ela_jpeg"
153
+ }
154
+
155
+ except Exception as e:
156
+ logger.warning(f"ELA detection failed: {e}")
157
+ return {
158
+ "signal_name": "ELA Compression Analysis",
159
+ "score": 0.5,
160
+ "confidence": 0.0,
161
+ "explanation": f"ELA analysis unavailable: {str(e)}",
162
+ "raw_value": 0.0,
163
+ "method": "ela_jpeg"
164
+ }
backend/tests/test_advanced_ai_detector.py CHANGED
@@ -67,4 +67,4 @@ def test_forensics_integration(sample_image_bytes):
67
  assert "ai_detection" in report
68
  assert "all_signals" in report["ai_detection"]
69
  # System has 21 signals: 19 statistical + 1 DIRE + 1 CLIP
70
- assert report["summary"]["total_detection_signals"] == 22
 
67
  assert "ai_detection" in report
68
  assert "all_signals" in report["ai_detection"]
69
  # System has 21 signals: 19 statistical + 1 DIRE + 1 CLIP
70
+ assert report["summary"]["total_detection_signals"] == 23
backend/tests/test_advanced_ensemble.py CHANGED
@@ -26,8 +26,8 @@ def test_advanced_ensemble_complete_detection(sample_image_bytes):
26
  assert "methods_used" in report
27
 
28
  # Should have 21 signals (19 statistical + DIRE + CLIP)
29
- assert report["total_signals"] == 22
30
- assert len(report["all_signals"]) == 22
31
 
32
  # Check methods used
33
  assert "statistical" in report["methods_used"]
@@ -36,7 +36,7 @@ def test_advanced_ensemble_complete_detection(sample_image_bytes):
36
  assert "prnu" in report["methods_used"]
37
 
38
  # Check version
39
- assert report["detection_version"] == "advanced-ensemble-v1.1"
40
 
41
  # Cleanup
42
  detector.cleanup()
@@ -50,7 +50,7 @@ def test_advanced_ensemble_forensics_integration(sample_image_bytes):
50
  report = forensics.generate_forensic_report()
51
 
52
  # Check advanced detection was used
53
- assert report["ai_detection"]["total_signals"] == 22
54
  assert report["metadata"]["analyzer_version"] == "6.0.0"
55
  assert "methods_used" in report["ai_detection"]
56
- assert len(report["ai_detection"]["methods_used"]) == 4
 
26
  assert "methods_used" in report
27
 
28
  # Should have 21 signals (19 statistical + DIRE + CLIP)
29
+ assert report["total_signals"] == 23
30
+ assert len(report["all_signals"]) == 23
31
 
32
  # Check methods used
33
  assert "statistical" in report["methods_used"]
 
36
  assert "prnu" in report["methods_used"]
37
 
38
  # Check version
39
+ assert report["detection_version"] == "advanced-ensemble-v1.2"
40
 
41
  # Cleanup
42
  detector.cleanup()
 
50
  report = forensics.generate_forensic_report()
51
 
52
  # Check advanced detection was used
53
+ assert report["ai_detection"]["total_signals"] == 23
54
  assert report["metadata"]["analyzer_version"] == "6.0.0"
55
  assert "methods_used" in report["ai_detection"]
56
+ assert len(report["ai_detection"]["methods_used"]) == 5
backend/tests/test_covariance_detector.py CHANGED
@@ -62,7 +62,7 @@ def test_covariance_forensics_integration(sample_image_bytes):
62
 
63
  assert "ai_detection" in report
64
  # System has 21 signals: 19 statistical + 1 DIRE + 1 CLIP
65
- assert report["ai_detection"]["total_signals"] == 22
66
  assert report["metadata"]["analyzer_version"] == "6.0.0"
67
  assert "detection_version" in report["ai_detection"]
68
 
 
62
 
63
  assert "ai_detection" in report
64
  # System has 21 signals: 19 statistical + 1 DIRE + 1 CLIP
65
+ assert report["ai_detection"]["total_signals"] == 23
66
  assert report["metadata"]["analyzer_version"] == "6.0.0"
67
  assert "detection_version" in report["ai_detection"]
68
 
backend/tests/test_determinism.py CHANGED
@@ -20,8 +20,8 @@ def test_detection_is_deterministic(sample_image_bytes):
20
  assert report1["summary"]["ai_classification"] == report2["summary"]["ai_classification"]
21
 
22
  # Signal counts should be identical
23
- assert report1["summary"]["total_detection_signals"] == 22
24
- assert report2["summary"]["total_detection_signals"] == 22
25
 
26
 
27
  def test_hash_generation_is_consistent(sample_image_bytes):
@@ -61,8 +61,8 @@ def test_forensic_report_stability(sample_image_bytes):
61
  assert report1["hashes"]["sha256"] == report2["hashes"]["sha256"]
62
 
63
  # Signal counts should be identical
64
- assert report1["summary"]["total_detection_signals"] == 22
65
- assert report2["summary"]["total_detection_signals"] == 22
66
  assert report1["summary"]["total_detection_signals"] == report2["summary"]["total_detection_signals"]
67
 
68
  # AI probability: allow 20% variance for CLIP randomness
@@ -114,8 +114,8 @@ def test_signal_ordering_is_stable(sample_image_bytes):
114
  assert "ai_detection" in report2
115
 
116
  # Both should have 21 signals total
117
- assert report1["ai_detection"]["total_signals"] == 22
118
- assert report2["ai_detection"]["total_signals"] == 22
119
 
120
  # Classification keys should be consistent
121
  assert report1["ai_detection"]["classification"] == report2["ai_detection"]["classification"]
 
20
  assert report1["summary"]["ai_classification"] == report2["summary"]["ai_classification"]
21
 
22
  # Signal counts should be identical
23
+ assert report1["summary"]["total_detection_signals"] == 23
24
+ assert report2["summary"]["total_detection_signals"] == 23
25
 
26
 
27
  def test_hash_generation_is_consistent(sample_image_bytes):
 
61
  assert report1["hashes"]["sha256"] == report2["hashes"]["sha256"]
62
 
63
  # Signal counts should be identical
64
+ assert report1["summary"]["total_detection_signals"] == 23
65
+ assert report2["summary"]["total_detection_signals"] == 23
66
  assert report1["summary"]["total_detection_signals"] == report2["summary"]["total_detection_signals"]
67
 
68
  # AI probability: allow 20% variance for CLIP randomness
 
114
  assert "ai_detection" in report2
115
 
116
  # Both should have 21 signals total
117
+ assert report1["ai_detection"]["total_signals"] == 23
118
+ assert report2["ai_detection"]["total_signals"] == 23
119
 
120
  # Classification keys should be consistent
121
  assert report1["ai_detection"]["classification"] == report2["ai_detection"]["classification"]
backend/tests/test_statistical_detector.py CHANGED
@@ -61,7 +61,7 @@ def test_statistical_forensics_integration(sample_image_bytes):
61
 
62
  assert "ai_detection" in report
63
  # System has 21 signals: 19 statistical + 1 DIRE + 1 CLIP
64
- assert report["ai_detection"]["total_signals"] == 22
65
  assert report["metadata"]["analyzer_version"] == "6.0.0"
66
  assert "detection_version" in report["ai_detection"]
67
 
 
61
 
62
  assert "ai_detection" in report
63
  # System has 21 signals: 19 statistical + 1 DIRE + 1 CLIP
64
+ assert report["ai_detection"]["total_signals"] == 23
65
  assert report["metadata"]["analyzer_version"] == "6.0.0"
66
  assert "detection_version" in report["ai_detection"]
67
 
backend/tests/test_ultra_advanced_detector.py CHANGED
@@ -60,6 +60,6 @@ def test_ultra_forensics_integration(sample_image_bytes):
60
 
61
  assert "ai_detection" in report
62
  # System has 21 signals: 19 statistical + 1 DIRE + 1 CLIP
63
- assert report["ai_detection"]["total_signals"] == 22
64
  assert report["metadata"]["analyzer_version"] == "6.0.0"
65
  assert "detection_version" in report["ai_detection"]
 
60
 
61
  assert "ai_detection" in report
62
  # System has 21 signals: 19 statistical + 1 DIRE + 1 CLIP
63
+ assert report["ai_detection"]["total_signals"] == 23
64
  assert report["metadata"]["analyzer_version"] == "6.0.0"
65
  assert "detection_version" in report["ai_detection"]
frontend/index.html CHANGED
@@ -122,7 +122,7 @@
122
  <nav class="navbar">
123
  <div class="nav-container">
124
  <div class="logo">VeriFile-X</div>
125
- <div class="nav-badge">22 Detection Signals • 96-98% Accuracy</div>
126
  </div>
127
  </nav>
128
 
 
122
  <nav class="navbar">
123
  <div class="nav-container">
124
  <div class="logo">VeriFile-X</div>
125
+ <div class="nav-badge">23 Detection Signals • 96-98% Accuracy</div>
126
  </div>
127
  </nav>
128