Skip to main content

phasm_core/stego/stc/
embed.rs

1// Copyright (c) 2026 Christoph Gaffga
2// SPDX-License-Identifier: GPL-3.0-only
3// https://github.com/cgaffga/phasmcore
4
5//! Viterbi-based STC embedding.
6//!
7//! Implements the forward (Viterbi) and backward (traceback) passes of the
8//! STC embedding algorithm. The encoder finds the minimum-cost stego bit
9//! sequence whose syndrome (under the H-hat matrix) matches the message.
10//!
11//! Two internal paths:
12//! - **Inline** (n ≤ 1M): stores all back pointers in one pass — fastest.
13//! - **Segmented** (n > 1M): checkpoint/recompute approach — O(√n) memory,
14//!   2× compute. Enables 48 MP+ images on memory-constrained devices.
15
16use super::hhat;
17use super::extract::stc_extract;
18use crate::stego::progress;
19
20/// Result of STC embedding: the stego bit sequence and its total distortion cost.
21pub struct EmbedResult {
22    pub stego_bits: Vec<u8>,
23    pub total_cost: f64,
24    /// Number of positions where cover bit != stego bit.
25    pub num_modifications: usize,
26}
27
28/// Number of progress steps reported during STC Viterbi embedding.
29///
30/// Distributed across the forward pass(es). STC is typically ~20-30% of
31/// total encode time on large images.
32pub const STC_PROGRESS_STEPS: u32 = 50;
33
34/// Back-pointer memory threshold (in cover positions). Above this, the
35/// segmented path is used. 1M positions × 16 bytes/u128 = 16 MB.
36const SEGMENTED_THRESHOLD: usize = 1_000_000;
37
38/// Embed a message into cover bits using the Viterbi-based STC algorithm.
39///
40/// Automatically selects the inline path (single-pass, O(n) memory) for
41/// small inputs, or the segmented path (checkpoint/recompute, O(√n) memory)
42/// for large inputs. Both paths produce identical output.
43///
44/// - `cover_bits`: LSBs of the cover coefficients (length n)
45/// - `costs`: cost of flipping each cover bit (length n). Use f32::INFINITY for WET.
46///   Promoted to f64 internally for accumulation precision.
47/// - `message`: message bits to embed (length m)
48/// - `hhat_matrix`: the H-hat submatrix (h rows × w columns)
49/// - `h`: constraint length (must be ≤ 7 so 2^h fits in u128)
50/// - `w`: submatrix width (should be ceil(n/m))
51///
52/// Returns the stego bit sequence that encodes the message with minimum
53/// distortion, or `None` if embedding is infeasible.
54///
55/// Reports [`STC_PROGRESS_STEPS`] progress sub-steps via [`progress::advance`]
56/// during the Viterbi forward pass(es).
57pub fn stc_embed(
58    cover_bits: &[u8],
59    costs: &[f32],
60    message: &[u8],
61    hhat_matrix: &[Vec<u32>],
62    h: usize,
63    w: usize,
64) -> Option<EmbedResult> {
65    // h ≤ 7 required: 2^7 = 128 states fit exactly in u128.
66    if w == 0 || h > 7 {
67        return None;
68    }
69
70    let n = cover_bits.len();
71    let m = message.len();
72
73    if m == 0 {
74        return Some(EmbedResult {
75            stego_bits: cover_bits.to_vec(),
76            total_cost: 0.0,
77            num_modifications: 0,
78        });
79    }
80
81    if n > SEGMENTED_THRESHOLD {
82        // §6E-C / Task #24.3 — route the segmented path through
83        // streaming-Viterbi via the InMemoryCoverFetch adapter.
84        // Bit-exact equivalent to the legacy `stc_embed_segmented`
85        // (verified by `streaming_matches_inline_segmented_large` in
86        // streaming_segmented.rs), so callers see no observable
87        // change. The win is the STC-internal O(√n) memory bound
88        // (checkpoint + back-pointer working set vs O(n) back-ptrs
89        // in the inline path) — relevant for long-clip video stego.
90        // A future per-GOP-replay CoverFetch adapter (v1.1+) will
91        // bound cover-side memory to O(√n) too.
92        use crate::stego::stc::streaming_segmented::{
93            stc_embed_streaming_segmented, InMemoryCoverFetch,
94        };
95        let k = ((m as f64).sqrt().ceil() as usize).max(1);
96        let mut cover = InMemoryCoverFetch::new(cover_bits, costs, m, w, k)?;
97        stc_embed_streaming_segmented(&mut cover, message, hhat_matrix, h, w).ok()
98    } else {
99        stc_embed_inline(cover_bits, costs, message, hhat_matrix, h, w)
100    }
101}
102
103// ---------------------------------------------------------------------------
104// Inline path: single forward pass, stores all back pointers.
105// Best for small/medium images where O(n) memory is acceptable.
106// ---------------------------------------------------------------------------
107
108fn stc_embed_inline(
109    cover_bits: &[u8],
110    costs: &[f32],
111    message: &[u8],
112    hhat_matrix: &[Vec<u32>],
113    h: usize,
114    w: usize,
115) -> Option<EmbedResult> {
116    let n = cover_bits.len();
117    let m = message.len();
118    let num_states = 1usize << h;
119    let inf = f64::INFINITY;
120
121    // Pre-compute H-hat columns (avoids repeated column_packed calls).
122    let columns: Vec<usize> = (0..w)
123        .map(|c| hhat::column_packed(hhat_matrix, c) as usize)
124        .collect();
125
126    // Progress: advance every n/STC_PROGRESS_STEPS elements.
127    let progress_interval = (n / STC_PROGRESS_STEPS as usize).max(1);
128
129    // Forward Viterbi pass with 1-bit packed back pointers.
130    // back_ptr[j] is a u128: bit s = 1 means stego_bit=1 was chosen for
131    // target state s (predecessor = s ^ col). 16 bytes per step.
132    //
133    // Pre-allocated cost buffers avoid per-iteration heap allocations.
134    // The target-state iteration writes every entry, so no fill needed
135    // for curr_cost. Only shifted_cost needs fill (sparse writes).
136    let mut prev_cost = vec![inf; num_states];
137    prev_cost[0] = 0.0;
138    let mut curr_cost = vec![0.0f64; num_states];
139    let mut shifted_cost = vec![inf; num_states];
140
141    let mut back_ptr: Vec<u128> = Vec::with_capacity(n);
142    let mut msg_idx = 0;
143
144    for j in 0..n {
145        let col_idx = j % w;
146        let col = columns[col_idx];
147        let flip_cost = costs[j] as f64; // promote f32→f64 for accumulation
148        let cover_bit = cover_bits[j] & 1;
149
150        let (cost_s0, cost_s1) = if cover_bit == 0 {
151            (0.0, flip_cost)
152        } else {
153            (flip_cost, 0.0)
154        };
155
156        let mut packed_bp = 0u128;
157
158        for s in 0..num_states {
159            let cost_0 = prev_cost[s] + cost_s0;
160            let cost_1 = prev_cost[s ^ col] + cost_s1;
161
162            if cost_1 < cost_0 {
163                curr_cost[s] = cost_1;
164                packed_bp |= 1u128 << s;
165            } else {
166                curr_cost[s] = cost_0;
167            }
168        }
169
170        back_ptr.push(packed_bp);
171
172        if col_idx == w - 1 && msg_idx < m {
173            let required_bit = message[msg_idx] as usize;
174            shifted_cost.fill(inf);
175
176            for s in 0..num_states {
177                if curr_cost[s] == inf { continue; }
178                if (s & 1) != required_bit { continue; }
179                let s_shifted = s >> 1;
180                if curr_cost[s] < shifted_cost[s_shifted] {
181                    shifted_cost[s_shifted] = curr_cost[s];
182                }
183            }
184
185            std::mem::swap(&mut prev_cost, &mut shifted_cost);
186            msg_idx += 1;
187        } else {
188            std::mem::swap(&mut prev_cost, &mut curr_cost);
189        }
190
191        if (j + 1) % progress_interval == 0 {
192            if progress::is_cancelled() { return None; }
193            progress::advance();
194        }
195    }
196
197    // Find terminal state with minimum cost.
198    let (best_state, best_cost) = find_best_state(&prev_cost);
199    if best_cost == inf { return None; }
200
201    // Backward traceback.
202    let mut stego_bits = vec![0u8; n];
203    let mut s = best_state;
204
205    for j in (0..n).rev() {
206        let col_idx = j % w;
207
208        if col_idx == w - 1 && (j / w) < m {
209            let msg_bit = message[j / w] as usize;
210            s = ((s << 1) | msg_bit) & (num_states - 1);
211        }
212
213        let bit = ((back_ptr[j] >> s) & 1) as u8;
214        stego_bits[j] = bit;
215
216        if bit == 1 {
217            s ^= columns[col_idx];
218        }
219    }
220
221    debug_assert_eq!(s, 0, "traceback did not return to initial state 0");
222    debug_assert_eq!(
223        stc_extract(&stego_bits, hhat_matrix, w)[..m],
224        message[..m],
225    );
226
227    let num_modifications = stego_bits.iter().zip(cover_bits.iter())
228        .filter(|(s, c)| s != c).count();
229
230    Some(EmbedResult { stego_bits, total_cost: best_cost, num_modifications })
231}
232
233// ---------------------------------------------------------------------------
234// Segmented path: checkpoint/recompute for O(√n) memory.
235// Two forward passes: one to save checkpoints, one to recompute segments.
236// ---------------------------------------------------------------------------
237
238fn stc_embed_segmented(
239    cover_bits: &[u8],
240    costs: &[f32],
241    message: &[u8],
242    hhat_matrix: &[Vec<u32>],
243    h: usize,
244    w: usize,
245) -> Option<EmbedResult> {
246    let n = cover_bits.len();
247    let m = message.len();
248    let num_states = 1usize << h;
249    let inf = f64::INFINITY;
250
251    // Pre-compute H-hat columns (avoids repeated column_packed calls
252    // across Phase A, Phase B forward, and Phase B traceback).
253    let columns: Vec<usize> = (0..w)
254        .map(|c| hhat::column_packed(hhat_matrix, c) as usize)
255        .collect();
256
257    // Checkpoint interval: K message blocks per segment.
258    // sqrt(m) balances checkpoint memory (K × 1 KB) with segment back_ptr
259    // memory (K × w × 16 bytes).
260    let k = ((m as f64).sqrt().ceil() as usize).max(1);
261    let num_segments = m.div_ceil(k);
262
263    // --- Phase A: forward scan, save checkpoints, no back_ptr ---
264    // Reports half the STC progress sub-steps.
265    let phase_a_steps = STC_PROGRESS_STEPS / 2;
266    let progress_interval_a = (n / phase_a_steps as usize).max(1);
267
268    // Pre-allocated cost buffers reused across all iterations.
269    let mut prev_cost = vec![inf; num_states];
270    prev_cost[0] = 0.0;
271    let mut curr_cost = vec![0.0f64; num_states];
272    let mut shifted_cost = vec![inf; num_states];
273
274    // checkpoint[s] = cost array at the START of segment s (post-shift from
275    // the preceding block, or the initial state for s=0).
276    let mut checkpoints: Vec<Vec<f64>> = Vec::with_capacity(num_segments);
277    checkpoints.push(prev_cost.clone());
278
279    let mut msg_idx = 0;
280
281    for j in 0..n {
282        let col_idx = j % w;
283        let col = columns[col_idx];
284        let flip_cost = costs[j] as f64; // promote f32→f64
285        let cover_bit = cover_bits[j] & 1;
286
287        let (cost_s0, cost_s1) = if cover_bit == 0 {
288            (0.0, flip_cost)
289        } else {
290            (flip_cost, 0.0)
291        };
292
293        for s in 0..num_states {
294            let cost_0 = prev_cost[s] + cost_s0;
295            let cost_1 = prev_cost[s ^ col] + cost_s1;
296            curr_cost[s] = if cost_1 < cost_0 { cost_1 } else { cost_0 };
297        }
298
299        if col_idx == w - 1 && msg_idx < m {
300            let required_bit = message[msg_idx] as usize;
301            shifted_cost.fill(inf);
302            for s in 0..num_states {
303                if curr_cost[s] == inf { continue; }
304                if (s & 1) != required_bit { continue; }
305                let s_shifted = s >> 1;
306                if curr_cost[s] < shifted_cost[s_shifted] {
307                    shifted_cost[s_shifted] = curr_cost[s];
308                }
309            }
310            std::mem::swap(&mut prev_cost, &mut shifted_cost);
311            msg_idx += 1;
312
313            // Save checkpoint at segment boundaries.
314            if msg_idx % k == 0 && msg_idx < m {
315                checkpoints.push(prev_cost.clone());
316            }
317        } else {
318            std::mem::swap(&mut prev_cost, &mut curr_cost);
319        }
320
321        if (j + 1) % progress_interval_a == 0 {
322            if progress::is_cancelled() { return None; }
323            progress::advance();
324        }
325    }
326
327    // Find terminal state with minimum cost.
328    let (best_state, best_cost) = find_best_state(&prev_cost);
329    if best_cost == inf { return None; }
330
331    // --- Phase B: segment-by-segment recomputation + traceback ---
332    // Reports the remaining STC progress sub-steps.
333    let phase_b_steps = STC_PROGRESS_STEPS - phase_a_steps;
334    let progress_interval_b = (n / phase_b_steps as usize).max(1);
335    let mut progress_counter = 0usize;
336
337    let mut stego_bits = vec![0u8; n];
338    let mut entry_state = best_state;
339
340    for seg in (0..num_segments).rev() {
341        let block_start = seg * k;
342        let block_end = ((seg + 1) * k).min(m);
343        let j_start = block_start * w;
344        let j_end = block_end * w;
345        let seg_len = j_end - j_start;
346
347        // Reset prev_cost from checkpoint (reuses the same buffer).
348        prev_cost.copy_from_slice(&checkpoints[seg]);
349
350        // Recompute forward Viterbi for this segment, storing back_ptr.
351        let mut seg_back_ptr: Vec<u128> = Vec::with_capacity(seg_len);
352        let mut seg_msg_idx = block_start;
353
354        for j in j_start..j_end {
355            let col_idx = j % w;
356            let col = columns[col_idx];
357            let flip_cost = costs[j] as f64; // promote f32→f64
358            let cover_bit = cover_bits[j] & 1;
359
360            let (cost_s0, cost_s1) = if cover_bit == 0 {
361                (0.0, flip_cost)
362            } else {
363                (flip_cost, 0.0)
364            };
365
366            let mut packed_bp = 0u128;
367
368            for s in 0..num_states {
369                let cost_0 = prev_cost[s] + cost_s0;
370                let cost_1 = prev_cost[s ^ col] + cost_s1;
371                if cost_1 < cost_0 {
372                    curr_cost[s] = cost_1;
373                    packed_bp |= 1u128 << s;
374                } else {
375                    curr_cost[s] = cost_0;
376                }
377            }
378
379            seg_back_ptr.push(packed_bp);
380
381            if col_idx == w - 1 && seg_msg_idx < m {
382                let required_bit = message[seg_msg_idx] as usize;
383                shifted_cost.fill(inf);
384                for s in 0..num_states {
385                    if curr_cost[s] == inf { continue; }
386                    if (s & 1) != required_bit { continue; }
387                    let s_shifted = s >> 1;
388                    if curr_cost[s] < shifted_cost[s_shifted] {
389                        shifted_cost[s_shifted] = curr_cost[s];
390                    }
391                }
392                std::mem::swap(&mut prev_cost, &mut shifted_cost);
393                seg_msg_idx += 1;
394            } else {
395                std::mem::swap(&mut prev_cost, &mut curr_cost);
396            }
397
398            progress_counter += 1;
399            if progress_counter.is_multiple_of(progress_interval_b) {
400                if progress::is_cancelled() { return None; }
401                progress::advance();
402            }
403        }
404
405        // Traceback within this segment.
406        let mut s = entry_state;
407        for local_j in (0..seg_len).rev() {
408            let j = j_start + local_j;
409            let col_idx = j % w;
410
411            if col_idx == w - 1 && (j / w) < m {
412                let msg_bit = message[j / w] as usize;
413                s = ((s << 1) | msg_bit) & (num_states - 1);
414            }
415
416            let bit = ((seg_back_ptr[local_j] >> s) & 1) as u8;
417            stego_bits[j] = bit;
418
419            if bit == 1 {
420                s ^= columns[col_idx];
421            }
422        }
423
424        // State at the start of this segment = entry state for previous segment.
425        entry_state = s;
426        // seg_back_ptr is dropped here, freeing the segment's memory.
427    }
428
429    debug_assert_eq!(entry_state, 0, "traceback did not return to initial state 0");
430    debug_assert_eq!(
431        stc_extract(&stego_bits, hhat_matrix, w)[..m],
432        message[..m],
433    );
434
435    let num_modifications = stego_bits.iter().zip(cover_bits.iter())
436        .filter(|(s, c)| s != c).count();
437
438    Some(EmbedResult { stego_bits, total_cost: best_cost, num_modifications })
439}
440
441// ---------------------------------------------------------------------------
442// Shared helpers
443// ---------------------------------------------------------------------------
444
445/// Find the state with minimum cost. Returns (state, cost).
446fn find_best_state(costs: &[f64]) -> (usize, f64) {
447    let mut best = 0;
448    let mut best_cost = f64::INFINITY;
449    for (s, &c) in costs.iter().enumerate() {
450        if c < best_cost {
451            best_cost = c;
452            best = s;
453        }
454    }
455    (best, best_cost)
456}
457
458#[cfg(test)]
459mod tests {
460    use super::*;
461    use super::super::hhat::generate_hhat;
462    use super::super::extract::stc_extract;
463
464    #[test]
465    fn embed_extract_roundtrip_tiny() {
466        let h = 3;
467        let n: usize = 20;
468        let m: usize = 4;
469        let w = n.div_ceil(m); // ceil(20/4) = 5
470        let seed = [42u8; 32];
471        let hhat = generate_hhat(h, w, &seed);
472
473        let cover_bits: Vec<u8> = (0..n).map(|i| (i % 2) as u8).collect();
474        let costs: Vec<f32> = vec![1.0; n];
475        let message = vec![1u8, 0, 1, 1];
476
477        let result = stc_embed(&cover_bits, &costs, &message, &hhat, h, w).unwrap();
478        assert_eq!(result.stego_bits.len(), n);
479
480        let extracted = stc_extract(&result.stego_bits, &hhat, w);
481        assert_eq!(&extracted[..m], &message[..]);
482    }
483
484    #[test]
485    fn embed_extract_roundtrip_h7() {
486        let h = 7;
487        let n: usize = 500;
488        let m: usize = 50;
489        let w = n.div_ceil(m);
490        let seed = [13u8; 32];
491        let hhat = generate_hhat(h, w, &seed);
492
493        let cover_bits: Vec<u8> = (0..n).map(|i| ((i * 7 + 3) % 2) as u8).collect();
494        let costs: Vec<f32> = (0..n).map(|i| 1.0 + (i as f32) * 0.01).collect();
495        let message: Vec<u8> = (0..m).map(|i| (i % 2) as u8).collect();
496
497        let result = stc_embed(&cover_bits, &costs, &message, &hhat, h, w).unwrap();
498        let extracted = stc_extract(&result.stego_bits, &hhat, w);
499        assert_eq!(&extracted[..m], &message[..]);
500    }
501
502    #[test]
503    fn wet_coefficients_not_modified() {
504        let h = 3;
505        let n: usize = 20;
506        let m: usize = 4;
507        let w = n.div_ceil(m);
508        let seed = [55u8; 32];
509        let hhat = generate_hhat(h, w, &seed);
510
511        let cover_bits: Vec<u8> = vec![0; n];
512        let mut costs: Vec<f32> = vec![1.0; n];
513        // Make positions 0, 5, 10, 15 WET
514        for i in (0..n).step_by(5) {
515            costs[i] = 1e13;
516        }
517        let message = vec![0u8, 1, 0, 1];
518
519        let result = stc_embed(&cover_bits, &costs, &message, &hhat, h, w).unwrap();
520
521        // WET positions must not change
522        for i in (0..n).step_by(5) {
523            assert_eq!(
524                result.stego_bits[i], cover_bits[i],
525                "WET position {i} was modified"
526            );
527        }
528
529        // Message still recoverable
530        let extracted = stc_extract(&result.stego_bits, &hhat, w);
531        assert_eq!(&extracted[..m], &message[..]);
532    }
533
534    #[test]
535    fn empty_message() {
536        let h = 3;
537        let n = 10;
538        let w = 5;
539        let seed = [0u8; 32];
540        let hhat = generate_hhat(h, w, &seed);
541
542        let cover_bits: Vec<u8> = vec![1; n];
543        let costs: Vec<f32> = vec![1.0; n];
544        let message: Vec<u8> = vec![];
545
546        let result = stc_embed(&cover_bits, &costs, &message, &hhat, h, w).unwrap();
547        assert_eq!(result.stego_bits, cover_bits);
548        assert_eq!(result.total_cost, 0.0);
549    }
550
551    /// Large synthetic test to verify 1-bit packed back pointers at scale.
552    #[test]
553    fn embed_extract_roundtrip_large() {
554        let h = 7;
555        let m = 10_000;
556        let w = 10;
557        let n = m * w; // 100K cover elements
558        let seed = [77u8; 32];
559        let hhat = generate_hhat(h, w, &seed);
560
561        let cover_bits: Vec<u8> = (0..n).map(|i| ((i * 31 + 17) % 2) as u8).collect();
562        let costs: Vec<f32> = (0..n).map(|i| {
563            let base = 0.5 + (i % 100) as f32 * 0.02;
564            if i % 500 == 0 { f32::INFINITY } else { base }
565        }).collect();
566        let message: Vec<u8> = (0..m).map(|i| ((i * 13 + 7) % 2) as u8).collect();
567
568        let result = stc_embed(&cover_bits, &costs, &message, &hhat, h, w).unwrap();
569        assert_eq!(result.stego_bits.len(), n);
570
571        let extracted = stc_extract(&result.stego_bits, &hhat, w);
572        assert_eq!(&extracted[..m], &message[..]);
573
574        for i in (0..n).step_by(500) {
575            assert_eq!(
576                result.stego_bits[i], cover_bits[i],
577                "WET position {i} was modified"
578            );
579        }
580    }
581
582    /// Verify that inline and segmented paths produce identical output.
583    #[test]
584    fn inline_segmented_equivalence() {
585        let h = 7;
586        let m = 500;
587        let w = 10;
588        let n = m * w; // 5000 cover elements
589        let seed = [99u8; 32];
590        let hhat = generate_hhat(h, w, &seed);
591
592        let cover_bits: Vec<u8> = (0..n).map(|i| ((i * 31 + 17) % 2) as u8).collect();
593        let costs: Vec<f32> = (0..n).map(|i| {
594            let base = 0.5 + (i % 100) as f32 * 0.02;
595            if i % 500 == 0 { f32::INFINITY } else { base }
596        }).collect();
597        let message: Vec<u8> = (0..m).map(|i| ((i * 13 + 7) % 2) as u8).collect();
598
599        let inline = stc_embed_inline(&cover_bits, &costs, &message, &hhat, h, w).unwrap();
600        let segmented = stc_embed_segmented(&cover_bits, &costs, &message, &hhat, h, w).unwrap();
601
602        assert_eq!(inline.stego_bits, segmented.stego_bits, "stego bits differ");
603        assert_eq!(inline.total_cost, segmented.total_cost, "total cost differs");
604    }
605
606    /// Equivalence test with a larger input covering multiple segments.
607    #[test]
608    fn inline_segmented_equivalence_large() {
609        let h = 7;
610        let m = 10_000;
611        let w = 10;
612        let n = m * w; // 100K cover elements, K ≈ 100 → ~100 segments
613        let seed = [88u8; 32];
614        let hhat = generate_hhat(h, w, &seed);
615
616        let cover_bits: Vec<u8> = (0..n).map(|i| ((i * 37 + 11) % 2) as u8).collect();
617        let costs: Vec<f32> = (0..n).map(|i| {
618            let base = 0.3 + (i % 200) as f32 * 0.01;
619            if i % 1000 == 0 { f32::INFINITY } else { base }
620        }).collect();
621        let message: Vec<u8> = (0..m).map(|i| ((i * 19 + 3) % 2) as u8).collect();
622
623        let inline = stc_embed_inline(&cover_bits, &costs, &message, &hhat, h, w).unwrap();
624        let segmented = stc_embed_segmented(&cover_bits, &costs, &message, &hhat, h, w).unwrap();
625
626        assert_eq!(inline.stego_bits, segmented.stego_bits, "stego bits differ");
627        assert_eq!(inline.total_cost, segmented.total_cost, "total cost differs");
628    }
629
630    /// Segmented path with a single segment (m ≤ K).
631    #[test]
632    fn segmented_single_segment() {
633        let h = 7;
634        let m = 4;
635        let w = 5;
636        let n = m * w;
637        let seed = [33u8; 32];
638        let hhat = generate_hhat(h, w, &seed);
639
640        let cover_bits: Vec<u8> = (0..n).map(|i| (i % 2) as u8).collect();
641        let costs: Vec<f32> = vec![1.0; n];
642        let message: Vec<u8> = vec![1, 0, 1, 1];
643
644        let inline = stc_embed_inline(&cover_bits, &costs, &message, &hhat, h, w).unwrap();
645        let segmented = stc_embed_segmented(&cover_bits, &costs, &message, &hhat, h, w).unwrap();
646
647        assert_eq!(inline.stego_bits, segmented.stego_bits);
648        assert_eq!(inline.total_cost, segmented.total_cost);
649    }
650}