1use crate::stego::armor::fft2d::Complex32;
12use rand::SeedableRng;
13use rand::Rng;
14use rand_chacha::ChaCha20Rng;
15
16use crate::stego::armor::fft2d::Spectrum2D;
17use crate::stego::crypto::derive_template_key;
18use crate::stego::error::StegoError;
19
20const K: usize = 32;
22
23const ALPHA: f32 = 0.4;
25
26const R_MIN_FACTOR: f64 = 0.05;
28
29const R_MAX_FACTOR: f64 = 0.25;
31
32const DETECTION_THRESHOLD: f64 = 3.0;
34
35const MIN_PEAKS_FOR_ESTIMATION: usize = 8;
37
38const SEARCH_RADIUS: i32 = 5;
40
41#[derive(Debug, Clone)]
43pub struct TemplatePeak {
44 pub u: f64,
46 pub v: f64,
48 pub amplitude: f64,
50}
51
52#[derive(Debug, Clone)]
54pub struct DetectedPeak {
55 pub expected_u: f64,
56 pub expected_v: f64,
57 pub detected_u: f64,
58 pub detected_v: f64,
59 pub confidence: f64,
60}
61
62#[derive(Debug, Clone)]
64pub struct AffineTransform {
65 pub rotation_rad: f64,
66 pub scale: f64,
67}
68
69pub fn generate_template_peaks(passphrase: &str, width: usize, height: usize) -> Result<Vec<TemplatePeak>, StegoError> {
74 let key = derive_template_key(passphrase)?;
75 let mut rng = ChaCha20Rng::from_seed(key);
76
77 let min_dim = width.min(height) as f64;
78 let r_min = R_MIN_FACTOR * min_dim;
79 let r_max = R_MAX_FACTOR * min_dim;
80
81 let mut peaks = Vec::with_capacity(K);
82 for _ in 0..K {
83 let angle: f64 = rng.gen_range(0.0..std::f64::consts::TAU);
85 let radius: f64 = rng.gen_range(r_min..r_max);
86
87 let (sin_a, cos_a) = crate::det_math::det_sincos(angle);
88 let u = radius * cos_a;
89 let v = radius * sin_a;
90
91 peaks.push(TemplatePeak {
92 u,
93 v,
94 amplitude: 0.0, });
96 }
97
98 Ok(peaks)
99}
100
101pub fn embed_template(spectrum: &mut Spectrum2D, peaks: &[TemplatePeak]) {
109 let w = spectrum.width;
110 let h = spectrum.height;
111 let cx = w as f64 / 2.0;
112 let cy = h as f64 / 2.0;
113
114 for peak in peaks {
115 let su = (cx + peak.u).round() as isize;
117 let sv = (cy + peak.v).round() as isize;
118
119 if su < 0 || su >= w as isize || sv < 0 || sv >= h as isize {
121 continue;
122 }
123 let idx = sv as usize * w + su as usize;
124
125 let local_mag = local_mean_magnitude(spectrum, su as usize, sv as usize);
127 let add_mag = ALPHA * local_mag.max(1.0);
128
129 let existing = spectrum.data[idx];
131 let enorm = crate::det_math::det_hypot(existing.re as f64, existing.im as f64) as f32;
132 let phase = if enorm > 1e-6 {
133 Complex32::new(existing.re / enorm, existing.im / enorm)
134 } else {
135 Complex32::new(1.0, 0.0)
136 };
137 spectrum.data[idx] += phase * add_mag;
138
139 let cu = (w as isize - su) % w as isize;
141 let cv = (h as isize - sv) % h as isize;
142 if cu >= 0 && cv >= 0 {
143 let conj_idx = cv as usize * w + cu as usize;
144 if conj_idx != idx {
145 let conj_existing = spectrum.data[conj_idx];
146 let cnorm = crate::det_math::det_hypot(conj_existing.re as f64, conj_existing.im as f64) as f32;
147 let conj_phase = if cnorm > 1e-6 {
148 Complex32::new(conj_existing.re / cnorm, conj_existing.im / cnorm)
149 } else {
150 Complex32::new(1.0, 0.0)
151 };
152 spectrum.data[conj_idx] += conj_phase * add_mag;
153 }
154 }
155 }
156}
157
158pub fn detect_template(spectrum: &Spectrum2D, peaks: &[TemplatePeak]) -> Vec<DetectedPeak> {
165 let w = spectrum.width;
166 let h = spectrum.height;
167 let cx = w as f64 / 2.0;
168 let cy = h as f64 / 2.0;
169
170 let magnitudes = super::fft2d::magnitude_spectrum(spectrum);
171
172 let mut detected = Vec::new();
173
174 for peak in peaks {
175 let su = (cx + peak.u).round() as isize;
176 let sv = (cy + peak.v).round() as isize;
177
178 if su < 0 || su >= w as isize || sv < 0 || sv >= h as isize {
179 continue;
180 }
181
182 let mut best_mag = 0.0f64;
184 let mut best_u = su;
185 let mut best_v = sv;
186 let mut noise_sum = 0.0f64;
187 let mut noise_sq_sum = 0.0f64;
188 let mut noise_count = 0usize;
189
190 for dv in -SEARCH_RADIUS..=SEARCH_RADIUS {
191 for du in -SEARCH_RADIUS..=SEARCH_RADIUS {
192 let nu = su + du as isize;
193 let nv = sv + dv as isize;
194 if nu < 0 || nu >= w as isize || nv < 0 || nv >= h as isize {
195 continue;
196 }
197 let mag = magnitudes[nv as usize * w + nu as usize] as f64;
198
199 if mag > best_mag {
200 best_mag = mag;
201 best_u = nu;
202 best_v = nv;
203 }
204
205 if du.abs() > 1 || dv.abs() > 1 {
207 noise_sum += mag;
208 noise_sq_sum += mag * mag;
209 noise_count += 1;
210 }
211 }
212 }
213
214 if noise_count < 2 {
215 continue;
216 }
217
218 let noise_mean = noise_sum / noise_count as f64;
219 let noise_var = (noise_sq_sum / noise_count as f64) - noise_mean * noise_mean;
220 let noise_std = noise_var.max(0.0).sqrt().max(1e-12);
222
223 let confidence = (best_mag - noise_mean) / noise_std;
224
225 if confidence >= DETECTION_THRESHOLD {
226 detected.push(DetectedPeak {
227 expected_u: peak.u,
228 expected_v: peak.v,
229 detected_u: best_u as f64 - cx,
230 detected_v: best_v as f64 - cy,
231 confidence,
232 });
233 }
234 }
235
236 detected
237}
238
239pub fn estimate_transform(detected: &[DetectedPeak]) -> Option<AffineTransform> {
252 if detected.len() < MIN_PEAKS_FOR_ESTIMATION {
253 return None;
254 }
255
256 let mut num_a = 0.0f64;
257 let mut num_b = 0.0f64;
258 let mut denom = 0.0f64;
259
260 for peak in detected {
261 let u = peak.expected_u;
262 let v = peak.expected_v;
263 let u_prime = peak.detected_u;
264 let v_prime = peak.detected_v;
265
266 num_a += u * u_prime + v * v_prime;
267 num_b += u * v_prime - v * u_prime;
268 denom += u * u + v * v;
269 }
270
271 if denom < 1e-12 {
272 return None;
273 }
274
275 let a = num_a / denom;
276 let b = num_b / denom;
277
278 let rotation_rad = crate::det_math::det_atan2(b, a);
279 let scale = (a * a + b * b).sqrt();
281
282 Some(AffineTransform {
283 rotation_rad,
284 scale,
285 })
286}
287
288fn local_mean_magnitude(spectrum: &Spectrum2D, u: usize, v: usize) -> f32 {
292 let w = spectrum.width;
293 let h = spectrum.height;
294 let mut sum = 0.0f64;
295 let mut count = 0;
296
297 for dv in -1i32..=1 {
298 for du in -1i32..=1 {
299 let nu = u as i32 + du;
300 let nv = v as i32 + dv;
301 if nu >= 0 && nu < w as i32 && nv >= 0 && nv < h as i32 {
302 let c = spectrum.data[nv as usize * w + nu as usize];
303 sum += crate::det_math::det_hypot(c.re as f64, c.im as f64);
304 count += 1;
305 }
306 }
307 }
308
309 if count > 0 { (sum / count as f64) as f32 } else { 1.0 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315 use crate::stego::armor::fft2d;
316
317 #[test]
318 fn generate_deterministic() {
319 let p1 = generate_template_peaks("test_pass", 256, 256).unwrap();
320 let p2 = generate_template_peaks("test_pass", 256, 256).unwrap();
321 assert_eq!(p1.len(), K);
322 assert_eq!(p2.len(), K);
323 for i in 0..K {
324 assert_eq!(p1[i].u, p2[i].u);
325 assert_eq!(p1[i].v, p2[i].v);
326 }
327 }
328
329 #[test]
330 fn different_passphrases_differ() {
331 let p1 = generate_template_peaks("pass1", 256, 256).unwrap();
332 let p2 = generate_template_peaks("pass2", 256, 256).unwrap();
333 let mut all_same = true;
335 for i in 0..K {
336 if (p1[i].u - p2[i].u).abs() > 1e-10 || (p1[i].v - p2[i].v).abs() > 1e-10 {
337 all_same = false;
338 break;
339 }
340 }
341 assert!(!all_same, "Different passphrases should produce different peaks");
342 }
343
344 #[test]
345 fn peaks_in_mid_frequency_band() {
346 let w = 256;
347 let h = 256;
348 let peaks = generate_template_peaks("test", w, h).unwrap();
349 let min_dim = w.min(h) as f64;
350 let r_min = R_MIN_FACTOR * min_dim;
351 let r_max = R_MAX_FACTOR * min_dim;
352
353 for (i, peak) in peaks.iter().enumerate() {
354 let r = (peak.u * peak.u + peak.v * peak.v).sqrt();
355 assert!(
356 r >= r_min - 0.01 && r <= r_max + 0.01,
357 "Peak {i} at radius {r} outside band [{r_min}, {r_max}]"
358 );
359 }
360 }
361
362 #[test]
363 fn embed_detect_roundtrip() {
364 let width = 128;
365 let height = 128;
366 let pixels: Vec<f64> = (0..width * height)
368 .map(|i| {
369 let x = (i % width) as f64;
370 let y = (i / width) as f64;
371 128.0 + 50.0 * (x * 0.1).sin() * (y * 0.05).cos()
372 })
373 .collect();
374
375 let mut spectrum = fft2d::fft2d(&pixels, width, height);
376 let peaks = generate_template_peaks("embed_test", width, height).unwrap();
377 embed_template(&mut spectrum, &peaks);
378
379 let detected = detect_template(&spectrum, &peaks);
380
381 assert!(
382 detected.len() >= K / 2,
383 "Should detect at least half of {} peaks, got {}",
384 K,
385 detected.len()
386 );
387 }
388
389 #[test]
390 fn transform_estimation_identity() {
391 let detected: Vec<DetectedPeak> = (0..16)
393 .map(|i| {
394 let angle = i as f64 * std::f64::consts::TAU / 16.0;
395 let r = 20.0;
396 let u = r * angle.cos();
397 let v = r * angle.sin();
398 DetectedPeak {
399 expected_u: u,
400 expected_v: v,
401 detected_u: u,
402 detected_v: v,
403 confidence: 10.0,
404 }
405 })
406 .collect();
407
408 let transform = estimate_transform(&detected).unwrap();
409 assert!(
410 transform.rotation_rad.abs() < 0.01,
411 "Expected ~0 rotation, got {}",
412 transform.rotation_rad.to_degrees()
413 );
414 assert!(
415 (transform.scale - 1.0).abs() < 0.01,
416 "Expected ~1.0 scale, got {}",
417 transform.scale
418 );
419 }
420
421 #[test]
422 fn transform_estimation_rotation() {
423 let angle_deg = 15.0;
425 let angle_rad = angle_deg * std::f64::consts::PI / 180.0;
426 let cos_a = angle_rad.cos();
427 let sin_a = angle_rad.sin();
428
429 let detected: Vec<DetectedPeak> = (0..20)
430 .map(|i| {
431 let a = i as f64 * std::f64::consts::TAU / 20.0;
432 let r = 25.0;
433 let u = r * a.cos();
434 let v = r * a.sin();
435 let u_rot = u * cos_a - v * sin_a;
437 let v_rot = u * sin_a + v * cos_a;
438 DetectedPeak {
439 expected_u: u,
440 expected_v: v,
441 detected_u: u_rot,
442 detected_v: v_rot,
443 confidence: 10.0,
444 }
445 })
446 .collect();
447
448 let transform = estimate_transform(&detected).unwrap();
449 assert!(
450 (transform.rotation_rad - angle_rad).abs() < 0.01,
451 "Expected {angle_deg} deg rotation, got {} deg",
452 transform.rotation_rad.to_degrees()
453 );
454 assert!(
455 (transform.scale - 1.0).abs() < 0.01,
456 "Expected ~1.0 scale, got {}",
457 transform.scale
458 );
459 }
460
461 #[test]
462 fn transform_estimation_scale() {
463 let scale_factor = 0.8;
465
466 let detected: Vec<DetectedPeak> = (0..16)
467 .map(|i| {
468 let a = i as f64 * std::f64::consts::TAU / 16.0;
469 let r = 20.0;
470 let u = r * a.cos();
471 let v = r * a.sin();
472 DetectedPeak {
473 expected_u: u,
474 expected_v: v,
475 detected_u: u * scale_factor,
476 detected_v: v * scale_factor,
477 confidence: 10.0,
478 }
479 })
480 .collect();
481
482 let transform = estimate_transform(&detected).unwrap();
483 assert!(
484 transform.rotation_rad.abs() < 0.01,
485 "Expected ~0 rotation, got {} deg",
486 transform.rotation_rad.to_degrees()
487 );
488 assert!(
489 (transform.scale - scale_factor).abs() < 0.01,
490 "Expected {scale_factor} scale, got {}",
491 transform.scale
492 );
493 }
494
495 #[test]
496 fn too_few_peaks_returns_none() {
497 let detected: Vec<DetectedPeak> = (0..3)
498 .map(|i| DetectedPeak {
499 expected_u: i as f64,
500 expected_v: i as f64,
501 detected_u: i as f64,
502 detected_v: i as f64,
503 confidence: 10.0,
504 })
505 .collect();
506
507 assert!(estimate_transform(&detected).is_none());
508 }
509}