anky2002 commited on
Commit
b7d09ad
·
verified ·
1 Parent(s): b500bb5

Upload agents/sensor_agent.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. agents/sensor_agent.py +239 -0
agents/sensor_agent.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FORENSIQ — Sensor Characteristics Agent
3
+ Analyzes sensor physics violations:
4
+ - PRNU (Photo-Response Non-Uniformity) noise residual analysis
5
+ - Noise structure (Poisson-Gaussian model fit)
6
+ - Bayer demosaicing artifact detection
7
+ """
8
+
9
+ import numpy as np
10
+ from PIL import Image
11
+ from scipy.ndimage import gaussian_filter, uniform_filter
12
+ from scipy.signal import convolve2d
13
+ from dataclasses import dataclass
14
+ from typing import Dict, Any
15
+
16
+ from agents.optical_agent import AgentEvidence
17
+
18
+
19
+ # ─── PRNU Noise Residual ────────────────────────────────────────────
20
+ def analyze_prnu(img: Image.Image) -> Dict[str, Any]:
21
+ """
22
+ Extract noise residual fingerprint.
23
+ Real cameras leave a unique PRNU pattern; AI images have uniform or random noise.
24
+ Inconsistent local noise variance → splicing / AI generation.
25
+ """
26
+ rgb = np.array(img.convert("RGB")).astype(np.float64)
27
+
28
+ noise_residuals = []
29
+ for c in range(3):
30
+ channel = rgb[:, :, c]
31
+ denoised = gaussian_filter(channel, sigma=3.0)
32
+ residual = channel - denoised
33
+ noise_residuals.append(residual)
34
+
35
+ noise = np.stack(noise_residuals, axis=-1)
36
+ noise_energy = np.mean(noise ** 2, axis=-1)
37
+
38
+ # Local variance map (32x32 blocks)
39
+ local_var = uniform_filter(noise_energy, size=32)
40
+
41
+ noise_std = float(np.std(local_var))
42
+ noise_mean = float(np.mean(local_var))
43
+ uniformity = 1.0 - min(noise_std / (noise_mean + 1e-9), 1.0)
44
+
45
+ # Correlation between channels (real sensors have correlated PRNU)
46
+ r_noise = noise_residuals[0].ravel()
47
+ g_noise = noise_residuals[1].ravel()
48
+ b_noise = noise_residuals[2].ravel()
49
+
50
+ # Subsample for speed
51
+ step = max(1, len(r_noise) // 100000)
52
+ rg_corr = float(np.corrcoef(r_noise[::step], g_noise[::step])[0, 1])
53
+ rb_corr = float(np.corrcoef(r_noise[::step], b_noise[::step])[0, 1])
54
+
55
+ # Real cameras: correlated noise residuals; AI: uncorrelated
56
+ avg_corr = (rg_corr + rb_corr) / 2
57
+
58
+ if uniformity > 0.7 and avg_corr > 0.3:
59
+ score = -0.4
60
+ note = "Consistent sensor noise pattern with correlated channels (real camera)"
61
+ elif uniformity < 0.4:
62
+ score = 0.5
63
+ note = "Inconsistent noise regions suggest splicing or AI generation"
64
+ elif avg_corr < 0.1:
65
+ score = 0.4
66
+ note = "Uncorrelated channel noise (atypical for real cameras)"
67
+ else:
68
+ score = 0.1
69
+ note = "Moderate noise consistency"
70
+
71
+ return {
72
+ "test": "PRNU Noise Residual",
73
+ "noise_uniformity": round(uniformity, 4),
74
+ "noise_mean": round(noise_mean, 4),
75
+ "rg_correlation": round(rg_corr, 4),
76
+ "rb_correlation": round(rb_corr, 4),
77
+ "score": score,
78
+ "note": note,
79
+ "noise_map": noise_energy,
80
+ }
81
+
82
+
83
+ # ─── Noise Structure (Poisson-Gaussian Model) ──────────────────────
84
+ def analyze_noise_structure(img: Image.Image) -> Dict[str, Any]:
85
+ """
86
+ Real sensor noise follows σ² = σ²_read + k·I (Poisson-Gaussian).
87
+ AI images lack this physical noise model.
88
+ """
89
+ rgb = np.array(img.convert("RGB")).astype(np.float64)
90
+ gray = np.mean(rgb, axis=-1)
91
+
92
+ # Compute local mean and local variance in blocks
93
+ block_size = 16
94
+ h, w = gray.shape
95
+ h_crop, w_crop = (h // block_size) * block_size, (w // block_size) * block_size
96
+ gray = gray[:h_crop, :w_crop]
97
+
98
+ intensities = []
99
+ variances = []
100
+
101
+ for i in range(0, h_crop, block_size):
102
+ for j in range(0, w_crop, block_size):
103
+ block = gray[i:i + block_size, j:j + block_size]
104
+ intensities.append(float(np.mean(block)))
105
+ variances.append(float(np.var(block)))
106
+
107
+ intensities = np.array(intensities)
108
+ variances = np.array(variances)
109
+
110
+ # Filter out extreme blocks
111
+ valid = (intensities > 10) & (intensities < 245) & (variances > 0)
112
+ if np.sum(valid) < 20:
113
+ return {
114
+ "test": "Noise Structure (Poisson-Gaussian)",
115
+ "score": 0.0,
116
+ "note": "Insufficient data for noise model fitting",
117
+ }
118
+
119
+ I = intensities[valid]
120
+ V = variances[valid]
121
+
122
+ # Fit linear model: V = a + b*I (Poisson-Gaussian)
123
+ try:
124
+ coeffs = np.polyfit(I, V, 1)
125
+ fitted = np.polyval(coeffs, I)
126
+ residual = float(np.mean((V - fitted) ** 2))
127
+ r_squared = 1.0 - residual / (np.var(V) + 1e-9)
128
+ except Exception:
129
+ r_squared = 0.0
130
+
131
+ if r_squared > 0.5:
132
+ score = -0.3
133
+ note = f"Noise follows Poisson-Gaussian model (R²={r_squared:.3f}, real sensor)"
134
+ elif r_squared < 0.1:
135
+ score = 0.5
136
+ note = f"Noise does NOT follow sensor physics (R²={r_squared:.3f}, AI-like)"
137
+ else:
138
+ score = 0.15
139
+ note = f"Weak Poisson-Gaussian fit (R²={r_squared:.3f})"
140
+
141
+ return {
142
+ "test": "Noise Structure (Poisson-Gaussian)",
143
+ "r_squared": round(r_squared, 4),
144
+ "slope": round(float(coeffs[0]), 6) if r_squared > 0 else None,
145
+ "intercept": round(float(coeffs[1]), 4) if r_squared > 0 else None,
146
+ "score": score,
147
+ "note": note,
148
+ }
149
+
150
+
151
+ # ─── Bayer Demosaicing Artifacts ────────────────────────────────────
152
+ def analyze_bayer_demosaicing(img: Image.Image) -> Dict[str, Any]:
153
+ """
154
+ Real cameras: green channel has ~2x samples → lower noise than R/B.
155
+ Expected: σ_green < σ_red ≈ σ_blue.
156
+ AI images lack this Bayer pattern artifact.
157
+ """
158
+ rgb = np.array(img.convert("RGB")).astype(np.float64)
159
+
160
+ # High-frequency noise per channel
161
+ noise_std = {}
162
+ for c, name in enumerate(["red", "green", "blue"]):
163
+ channel = rgb[:, :, c]
164
+ denoised = gaussian_filter(channel, sigma=1.5)
165
+ noise = channel - denoised
166
+ noise_std[name] = float(np.std(noise))
167
+
168
+ green_lower = noise_std["green"] < min(noise_std["red"], noise_std["blue"])
169
+ rb_similar = abs(noise_std["red"] - noise_std["blue"]) / (
170
+ max(noise_std["red"], noise_std["blue"]) + 1e-9
171
+ ) < 0.2
172
+
173
+ if green_lower and rb_similar:
174
+ score = -0.4
175
+ note = (
176
+ f"Bayer pattern detected: σ_green({noise_std['green']:.3f}) < "
177
+ f"σ_red({noise_std['red']:.3f}) ≈ σ_blue({noise_std['blue']:.3f})"
178
+ )
179
+ elif green_lower:
180
+ score = -0.2
181
+ note = "Green channel is quieter but R/B difference is large"
182
+ else:
183
+ score = 0.4
184
+ note = (
185
+ f"No Bayer pattern: σ_green({noise_std['green']:.3f}), "
186
+ f"σ_red({noise_std['red']:.3f}), σ_blue({noise_std['blue']:.3f})"
187
+ )
188
+
189
+ return {
190
+ "test": "Bayer Demosaicing Artifacts",
191
+ "noise_red": round(noise_std["red"], 4),
192
+ "noise_green": round(noise_std["green"], 4),
193
+ "noise_blue": round(noise_std["blue"], 4),
194
+ "green_is_lower": green_lower,
195
+ "rb_similar": rb_similar,
196
+ "score": score,
197
+ "note": note,
198
+ }
199
+
200
+
201
+ # ─── Main Agent Entry Point ─────────────────────────────────────────
202
+ def run_sensor_agent(img: Image.Image) -> AgentEvidence:
203
+ """Run all sensor characteristic tests."""
204
+ findings = []
205
+ scores = []
206
+
207
+ for fn in [analyze_prnu, analyze_noise_structure, analyze_bayer_demosaicing]:
208
+ try:
209
+ result = fn(img)
210
+ findings.append(result)
211
+ scores.append(result["score"])
212
+ except Exception as e:
213
+ findings.append({"test": fn.__name__, "error": str(e), "score": 0})
214
+
215
+ avg_score = float(np.mean(scores)) if scores else 0.0
216
+ confidence = min(1.0, 0.5 + 0.5 * abs(avg_score))
217
+
218
+ violations = [f["test"] for f in findings if f.get("score", 0) > 0.2]
219
+ compliant = [f["test"] for f in findings if f.get("score", 0) < -0.1]
220
+
221
+ if violations:
222
+ rationale = f"Sensor violations: {', '.join(violations)}."
223
+ elif compliant:
224
+ rationale = f"Sensor physics consistent: {', '.join(compliant)}."
225
+ else:
226
+ rationale = "Sensor analysis inconclusive."
227
+
228
+ for f in findings:
229
+ if f.get("note"):
230
+ rationale += f" [{f['test']}]: {f['note']}."
231
+
232
+ return AgentEvidence(
233
+ agent_name="Sensor Characteristics Agent",
234
+ violation_score=np.clip(avg_score, -1, 1),
235
+ confidence=confidence,
236
+ failure_prob=max(0.0, 1.0 - len(scores) / 3),
237
+ rationale=rationale,
238
+ sub_findings=findings,
239
+ )