abinazebinoy commited on
Commit
bbdd09d
·
1 Parent(s): 8d41abf

fix(F-16d): CovarianceDetector is now a facade, no longer inherits UltraAdvancedDetector

Browse files

Composes BasicSignals + UltraSignals + CovarianceSignals. Re-exposes
all 16 methods (13 'inherited' + 3 own).

Verified: all 7 tests in test_covariance_detector.py pass unchanged.

backend/services/covariance_detector.py CHANGED
@@ -1,334 +1,113 @@
1
  """
2
  Advanced Covariance & Eigenvalue Analysis for AI Detection
3
  Implements cutting-edge forensic methods based on sensor physics.
 
 
 
 
4
  """
5
- import numpy as np
6
- import cv2
7
  from typing import Dict, Any
8
- from scipy.linalg import eig
9
 
10
  from backend.core.logger import setup_logger
11
- from backend.services.ultra_advanced_detector import UltraAdvancedDetector
 
 
12
 
13
  logger = setup_logger(__name__)
14
 
15
 
16
- class CovarianceDetector(UltraAdvancedDetector):
17
  """
18
  Extends UltraAdvancedDetector with eigenvalue and local covariance analysis.
19
-
20
  New methods:
21
  1. Covariance eigenvalue spread analysis
22
  2. Local covariance consistency across patches
23
  3. Patch-level anisotropy variance
24
  """
25
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  def analyze_eigenvalue_spread(self) -> Dict[str, Any]:
27
- """
28
- Covariance Matrix Eigenvalue Analysis
29
-
30
- Research basis: Real cameras have dominant principal component in noise
31
-
32
- Method:
33
- 1. Extract RGB noise residuals using Laplacian
34
- 2. Compute 3×3 covariance matrix
35
- 3. Calculate eigenvalues λ₁, λ₂, λ₃
36
- 4. Measure spread ratio: λ₁ / λ₃
37
-
38
- Real cameras: High spread (λ₁ >> λ₃) - dominant shared noise
39
- AI images: Lower spread - more uniform eigenvalues
40
-
41
- Returns:
42
- Detection signal with score, confidence, explanation
43
- """
44
- # Split RGB channels
45
- b, g, r = cv2.split(self.cv_image.astype(float))
46
-
47
- # Extract high-frequency noise using Laplacian
48
- r_noise = cv2.Laplacian(r, cv2.CV_64F).flatten()
49
- g_noise = cv2.Laplacian(g, cv2.CV_64F).flatten()
50
- b_noise = cv2.Laplacian(b, cv2.CV_64F).flatten()
51
-
52
- # Sample for computational efficiency (use 10k pixels max)
53
- if len(r_noise) > 10000:
54
- # Use content-derived seed for determinism — same image always
55
- # produces the same sample even under concurrent requests.
56
- import hashlib as _hl
57
- _seed = int(_hl.sha256(self.image_bytes[:64]).hexdigest()[:8], 16) % (2**31)
58
- _rng = np.random.default_rng(_seed)
59
- indices = _rng.choice(len(r_noise), 10000, replace=False)
60
- r_noise = r_noise[indices]
61
- g_noise = g_noise[indices]
62
- b_noise = b_noise[indices]
63
-
64
- # Stack into matrix
65
- noise_matrix = np.vstack([r_noise, g_noise, b_noise])
66
-
67
- # Compute covariance matrix (3×3)
68
- cov_matrix = np.cov(noise_matrix)
69
-
70
- # Calculate eigenvalues
71
- eigenvalues, _ = eig(cov_matrix)
72
- eigenvalues = np.real(eigenvalues) # Take real part
73
- eigenvalues = np.sort(eigenvalues)[::-1] # Sort descending
74
-
75
- # Compute spread ratio
76
- if eigenvalues[2] < 1e-10:
77
- spread_ratio = 100.0 # Very high spread
78
- else:
79
- spread_ratio = eigenvalues[0] / eigenvalues[2]
80
-
81
- # Real cameras: spread typically 5-20
82
- # AI images: often 2-8 (more uniform)
83
- if spread_ratio < 4.0:
84
- score = (4.0 - spread_ratio) / 4.0
85
- explanation = f"Eigenvalue spread ({spread_ratio:.2f}) is low - noise lacks dominant component"
86
- elif spread_ratio > 25.0:
87
- score = min(1.0, (spread_ratio - 25.0) / 20.0)
88
- explanation = f"Eigenvalue spread ({spread_ratio:.2f}) is abnormally high"
89
- else:
90
- score = 0.0
91
- explanation = f"Eigenvalue spread ({spread_ratio:.2f}) matches camera sensor physics"
92
-
93
- logger.info(f"Eigenvalue spread: λ₁={eigenvalues[0]:.2f}, λ₃={eigenvalues[2]:.2f}, ratio={spread_ratio:.2f}")
94
-
95
- return {
96
- "signal_name": "Eigenvalue Spread",
97
- "score": float(min(1.0, score)),
98
- "confidence": 0.90,
99
- "explanation": explanation,
100
- "raw_value": float(spread_ratio),
101
- "expected_range": "4.0-25.0",
102
- "method": "covariance_eigenvalue_analysis"
103
- }
104
-
105
  def analyze_local_covariance_consistency(self) -> Dict[str, Any]:
106
- """
107
- Local Covariance Consistency Analysis
108
-
109
- Research basis: Real cameras have consistent physical covariance patterns
110
-
111
- Method:
112
- 1. Divide image into patches (64×64)
113
- 2. Compute RGB noise covariance per patch
114
- 3. Measure variance of covariances across patches
115
-
116
- Real cameras: Consistent covariance structure
117
- AI images: Irregular spatial variation
118
-
119
- Returns:
120
- Detection signal with score, confidence, explanation
121
- """
122
- patch_size = 64
123
- covariances = []
124
-
125
- # Divide into patches
126
- for i in range(0, self.height - patch_size, patch_size):
127
- for j in range(0, self.width - patch_size, patch_size):
128
- patch = self.cv_image[i:i+patch_size, j:j+patch_size]
129
-
130
- # Extract RGB
131
- b, g, r = cv2.split(patch.astype(float))
132
-
133
- # Noise residuals
134
- r_noise = cv2.Laplacian(r, cv2.CV_64F).flatten()
135
- g_noise = cv2.Laplacian(g, cv2.CV_64F).flatten()
136
- b_noise = cv2.Laplacian(b, cv2.CV_64F).flatten()
137
-
138
- # Covariance matrix
139
- noise_matrix = np.vstack([r_noise, g_noise, b_noise])
140
- cov_matrix = np.cov(noise_matrix)
141
-
142
- # Store mean off-diagonal correlation
143
- mean_corr = (abs(cov_matrix[0,1]) + abs(cov_matrix[0,2]) + abs(cov_matrix[1,2])) / 3
144
- covariances.append(mean_corr)
145
-
146
- if len(covariances) < 4:
147
- return {
148
- "signal_name": "Local Covariance Consistency",
149
- "score": 0.0,
150
- "confidence": 0.3,
151
- "explanation": "Image too small for patch covariance analysis",
152
- "raw_value": 0.0,
153
- "expected_range": "N/A",
154
- "method": "local_covariance_consistency"
155
- }
156
-
157
- # Compute coefficient of variation (CV = std/mean)
158
- covariances = np.array(covariances)
159
- mean_cov = covariances.mean()
160
- std_cov = covariances.std()
161
-
162
- if mean_cov < 1e-6:
163
- cv = 0.0
164
- else:
165
- cv = std_cov / mean_cov
166
-
167
- # Real cameras: CV typically 0.1-0.4 (consistent)
168
- # AI images: often > 0.5 (irregular)
169
- if cv > 0.45:
170
- score = min(1.0, (cv - 0.45) / 0.3)
171
- explanation = f"Covariance consistency (CV={cv:.3f}) is irregular across patches"
172
- else:
173
- score = 0.0
174
- explanation = f"Covariance consistency (CV={cv:.3f}) is natural"
175
-
176
- logger.info(f"Local covariance: mean={mean_cov:.4f}, std={std_cov:.4f}, CV={cv:.3f}")
177
-
178
- return {
179
- "signal_name": "Local Covariance Consistency",
180
- "score": float(score),
181
- "confidence": 0.85,
182
- "explanation": explanation,
183
- "raw_value": float(cv),
184
- "expected_range": "< 0.45",
185
- "method": "local_covariance_consistency"
186
- }
187
-
188
  def analyze_patch_anisotropy_variance(self) -> Dict[str, Any]:
189
- """
190
- Patch-Level Anisotropy Variance Analysis
191
-
192
- Research basis: Natural images have varying directional energy
193
-
194
- Method:
195
- 1. Divide into patches
196
- 2. Compute directional energy (horizontal vs vertical) per patch
197
- 3. Measure variance of anisotropy across patches
198
-
199
- Anisotropy = |E_horizontal - E_vertical| / (E_horizontal + E_vertical)
200
-
201
- Real images: High variance (different textures/edges)
202
- AI images: Lower variance (more isotropic generation)
203
-
204
- Returns:
205
- Detection signal with score, confidence, explanation
206
- """
207
- patch_size = 64
208
- anisotropies = []
209
-
210
- # Divide into patches
211
- for i in range(0, self.height - patch_size, patch_size):
212
- for j in range(0, self.width - patch_size, patch_size):
213
- patch = self.cv_gray[i:i+patch_size, j:j+patch_size].astype(float)
214
-
215
- # Compute gradients
216
- grad_x = cv2.Sobel(patch, cv2.CV_64F, 1, 0, ksize=3)
217
- grad_y = cv2.Sobel(patch, cv2.CV_64F, 0, 1, ksize=3)
218
-
219
- # Energy in each direction
220
- energy_h = np.sum(grad_x ** 2)
221
- energy_v = np.sum(grad_y ** 2)
222
-
223
- # Anisotropy measure
224
- total_energy = energy_h + energy_v
225
- if total_energy > 1e-6:
226
- anisotropy = abs(energy_h - energy_v) / total_energy
227
- else:
228
- anisotropy = 0.0
229
-
230
- anisotropies.append(anisotropy)
231
-
232
- if len(anisotropies) < 4:
233
- return {
234
- "signal_name": "Patch Anisotropy Variance",
235
- "score": 0.0,
236
- "confidence": 0.3,
237
- "explanation": "Image too small for anisotropy analysis",
238
- "raw_value": 0.0,
239
- "expected_range": "N/A",
240
- "method": "patch_anisotropy_variance"
241
- }
242
-
243
- # Compute variance of anisotropy
244
- anisotropy_var = np.var(anisotropies)
245
-
246
- # Real images: variance typically 0.02-0.08
247
- # AI images: often < 0.015 (more uniform)
248
- if anisotropy_var < 0.015:
249
- score = (0.015 - anisotropy_var) / 0.015
250
- explanation = f"Anisotropy variance ({anisotropy_var:.4f}) is too low - uniform directionality"
251
- else:
252
- score = 0.0
253
- explanation = f"Anisotropy variance ({anisotropy_var:.4f}) shows natural variation"
254
-
255
- logger.info(f"Anisotropy: mean={np.mean(anisotropies):.4f}, var={anisotropy_var:.4f}")
256
-
257
- return {
258
- "signal_name": "Patch Anisotropy Variance",
259
- "score": float(min(1.0, score)),
260
- "confidence": 0.82,
261
- "explanation": explanation,
262
- "raw_value": float(anisotropy_var),
263
- "expected_range": "> 0.015",
264
- "method": "patch_anisotropy_variance"
265
- }
266
-
267
  def detect(self) -> Dict[str, Any]:
268
  """
269
  Run complete covariance detection with all methods.
270
-
271
  Returns:
272
  Complete report with 16 detection signals
273
  """
274
  logger.info(f"Starting covariance detection for {self.filename}")
275
-
276
- # Run parent class methods (13 signals)
277
- base_report = super().detect()
278
-
279
- # Add new covariance methods (3 signals)
280
- new_signals = [
281
- self.analyze_eigenvalue_spread(),
282
- self.analyze_local_covariance_consistency(),
283
- self.analyze_patch_anisotropy_variance()
284
- ]
285
-
286
- # Combine all signals
287
- all_signals = base_report["all_signals"] + new_signals
288
-
289
- # Recalculate final score with all 16 signals
290
- total_weight = sum(s["confidence"] for s in all_signals)
291
- weighted_score = sum(s["score"] * s["confidence"] for s in all_signals) / total_weight
292
-
293
- suspicious_count = sum(1 for s in all_signals if s["score"] > 0.5)
294
-
295
- # Boost if multiple signals agree (more aggressive with 16 signals)
296
- if suspicious_count >= 8:
297
- weighted_score = min(1.0, weighted_score * 1.3)
298
- elif suspicious_count >= 6:
299
- weighted_score = min(1.0, weighted_score * 1.2)
300
-
301
- # Classification
302
- if weighted_score > 0.75:
303
- classification = "likely_ai_generated"
304
- confidence = "high"
305
- elif weighted_score > 0.45:
306
- classification = "possibly_ai_generated"
307
- confidence = "medium"
308
- else:
309
- classification = "likely_authentic"
310
- confidence = "high" if weighted_score < 0.25 else "medium"
311
-
312
- # Top reasons
313
- sorted_signals = sorted(all_signals, key=lambda x: x["score"], reverse=True)
314
- top_reasons = [s["explanation"] for s in sorted_signals[:3]]
315
-
316
- result = {
317
- "ai_probability": float(weighted_score),
318
- "classification": classification,
319
- "confidence": confidence,
320
- "suspicious_signals_count": suspicious_count,
321
- "total_signals": len(all_signals),
322
- "all_signals": all_signals,
323
- "top_reasons": top_reasons,
324
- "summary": f"Analyzed using {len(all_signals)} independent signals. "
325
- f"{suspicious_count} signals indicate AI generation.",
326
- "detection_version": "covariance-advanced-v1.0"
327
- }
328
-
329
  logger.info(
330
- f"Covariance detection complete: {classification} "
331
- f"(p={weighted_score:.3f}, {suspicious_count}/{len(all_signals)} signals)"
 
332
  )
333
-
334
  return result
 
1
  """
2
  Advanced Covariance & Eigenvalue Analysis for AI Detection
3
  Implements cutting-edge forensic methods based on sensor physics.
4
+
5
+ F-16: composition-based facade, see advanced_ai_detector.py's module
6
+ docstring for the full rationale. Composes BasicSignals + UltraSignals
7
+ + CovarianceSignals and re-exposes every method from all three.
8
  """
 
 
9
  from typing import Dict, Any
 
10
 
11
  from backend.core.logger import setup_logger
12
+ from backend.services.statistical_signals import (
13
+ ImageContext, BasicSignals, UltraSignals, CovarianceSignals, score_and_classify
14
+ )
15
 
16
  logger = setup_logger(__name__)
17
 
18
 
19
+ class CovarianceDetector:
20
  """
21
  Extends UltraAdvancedDetector with eigenvalue and local covariance analysis.
22
+
23
  New methods:
24
  1. Covariance eigenvalue spread analysis
25
  2. Local covariance consistency across patches
26
  3. Patch-level anisotropy variance
27
  """
28
+
29
+ def __init__(self, image_bytes: bytes, filename: str):
30
+ self.ctx = ImageContext.from_bytes(image_bytes, filename)
31
+
32
+ self.image_bytes = self.ctx.image_bytes
33
+ self.filename = self.ctx.filename
34
+ self.pil_image = self.ctx.pil_image
35
+ self.cv_image = self.ctx.cv_image
36
+ self.cv_gray = self.ctx.cv_gray
37
+ self.height, self.width = self.ctx.height, self.ctx.width
38
+
39
+ self._basic = BasicSignals(self.ctx)
40
+ self._ultra = UltraSignals(self.ctx)
41
+ self._cov = CovarianceSignals(self.ctx)
42
+
43
+ logger.info(f"Initialized advanced detector for {filename} ({self.width}x{self.height}px)")
44
+
45
+ # -- inherited signal methods (base + ultra levels) --
46
+ def analyze_fft_radial_spectrum(self) -> Dict[str, Any]:
47
+ return self._basic.analyze_fft_radial_spectrum()
48
+
49
+ def analyze_dct_coefficients(self) -> Dict[str, Any]:
50
+ return self._basic.analyze_dct_coefficients()
51
+
52
+ def analyze_wavelet_energy(self) -> Dict[str, Any]:
53
+ return self._basic.analyze_wavelet_energy()
54
+
55
+ def analyze_glcm_texture(self) -> Dict[str, Any]:
56
+ return self._basic.analyze_glcm_texture()
57
+
58
+ def analyze_noise_residual(self) -> Dict[str, Any]:
59
+ return self._basic.analyze_noise_residual()
60
+
61
+ def analyze_spectral_entropy(self) -> Dict[str, Any]:
62
+ return self._basic.analyze_spectral_entropy()
63
+
64
+ def analyze_lbp_texture(self) -> Dict[str, Any]:
65
+ return self._basic.analyze_lbp_texture()
66
+
67
+ def analyze_edge_statistics(self) -> Dict[str, Any]:
68
+ return self._basic.analyze_edge_statistics()
69
+
70
+ def analyze_color_correlation(self) -> Dict[str, Any]:
71
+ return self._basic.analyze_color_correlation()
72
+
73
+ def analyze_compression_artifacts(self) -> Dict[str, Any]:
74
+ return self._basic.analyze_compression_artifacts()
75
+
76
+ def analyze_rgb_noise_covariance(self) -> Dict[str, Any]:
77
+ return self._ultra.analyze_rgb_noise_covariance()
78
+
79
+ def analyze_patch_spectral_variance(self) -> Dict[str, Any]:
80
+ return self._ultra.analyze_patch_spectral_variance()
81
+
82
+ def analyze_natural_prior_deviation(self) -> Dict[str, Any]:
83
+ return self._ultra.analyze_natural_prior_deviation()
84
+
85
+ # -- this class's own 3 signal methods --
86
  def analyze_eigenvalue_spread(self) -> Dict[str, Any]:
87
+ return self._cov.analyze_eigenvalue_spread()
88
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  def analyze_local_covariance_consistency(self) -> Dict[str, Any]:
90
+ return self._cov.analyze_local_covariance_consistency()
91
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  def analyze_patch_anisotropy_variance(self) -> Dict[str, Any]:
93
+ return self._cov.analyze_patch_anisotropy_variance()
94
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  def detect(self) -> Dict[str, Any]:
96
  """
97
  Run complete covariance detection with all methods.
98
+
99
  Returns:
100
  Complete report with 16 detection signals
101
  """
102
  logger.info(f"Starting covariance detection for {self.filename}")
103
+
104
+ signals = self._basic.compute_all() + self._ultra.compute_all() + self._cov.compute_all()
105
+ result = score_and_classify(signals, [(8, 1.3), (6, 1.2)], "covariance-advanced-v1.0")
106
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  logger.info(
108
+ f"Covariance detection complete: {result['classification']} "
109
+ f"(p={result['ai_probability']:.3f}, "
110
+ f"{result['suspicious_signals_count']}/{result['total_signals']} signals)"
111
  )
112
+
113
  return result