File size: 5,192 Bytes
f2f99a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86323af
 
 
 
 
 
 
f2f99a3
86323af
f2f99a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
/**
 * Tile layout and autocorrelation-based sync recovery
 *
 * The watermark is embedded as a periodic pattern of tiles.
 * Each tile contains one complete copy of the coded payload.
 * During detection, autocorrelation recovers the tile grid
 * even after cropping.
 */

import type { Buffer2D } from './types.js';

/** Tile grid description */
export interface TileGrid {
  /** Tile period in subband pixels */
  tilePeriod: number;
  /** Phase offset X (for cropped frames) */
  phaseX: number;
  /** Phase offset Y (for cropped frames) */
  phaseY: number;
  /** Number of complete tiles in X direction */
  tilesX: number;
  /** Number of complete tiles in Y direction */
  tilesY: number;
  /** Total number of tiles */
  totalTiles: number;
}

/**
 * Compute the tile grid for a given subband size and tile period
 * During embedding, phase is always (0, 0)
 */
export function computeTileGrid(
  subbandWidth: number,
  subbandHeight: number,
  tilePeriod: number
): TileGrid {
  // Snap tile period down to a multiple of 8 so that DCT blocks
  // perfectly tile each tile with no leftover strips (which would
  // show up as unembedded grid lines in the diff).
  const snapped = Math.floor(tilePeriod / 8) * 8;
  const effectivePeriod = Math.max(8, snapped);
  const tilesX = Math.floor(subbandWidth / effectivePeriod);
  const tilesY = Math.floor(subbandHeight / effectivePeriod);
  return {
    tilePeriod: effectivePeriod,
    phaseX: 0,
    phaseY: 0,
    tilesX,
    tilesY,
    totalTiles: tilesX * tilesY,
  };
}

/**
 * Get the subband region for a specific tile
 * Returns [startX, startY] in subband coordinates
 */
export function getTileOrigin(grid: TileGrid, tileIdx: number): { x: number; y: number } {
  const tileCol = tileIdx % grid.tilesX;
  const tileRow = Math.floor(tileIdx / grid.tilesX);
  return {
    x: grid.phaseX + tileCol * grid.tilePeriod,
    y: grid.phaseY + tileRow * grid.tilePeriod,
  };
}

/**
 * Compute 8x8 DCT block positions within a tile
 * Returns array of [blockRow, blockCol] in subband coordinates
 */
export function getTileBlocks(
  tileOriginX: number,
  tileOriginY: number,
  tilePeriod: number,
  subbandWidth: number,
  subbandHeight: number
): Array<{ row: number; col: number }> {
  const blocks: Array<{ row: number; col: number }> = [];
  const blocksPerTileSide = Math.floor(tilePeriod / 8);

  for (let br = 0; br < blocksPerTileSide; br++) {
    for (let bc = 0; bc < blocksPerTileSide; bc++) {
      const absRow = Math.floor(tileOriginY / 8) + br;
      const absCol = Math.floor(tileOriginX / 8) + bc;
      // Ensure the block fits within the subband
      if ((absRow + 1) * 8 <= subbandHeight && (absCol + 1) * 8 <= subbandWidth) {
        blocks.push({ row: absRow, col: absCol });
      }
    }
  }

  return blocks;
}

/**
 * Autocorrelation-based tile period and phase recovery
 *
 * Computes 2D autocorrelation of the subband energy pattern
 * to find the periodic tile structure.
 */
export function recoverTileGrid(
  subband: Buffer2D,
  expectedTilePeriod: number,
  searchRange: number = 4
): TileGrid {
  const { data, width, height } = subband;

  // Compute block energy map (8x8 blocks)
  const bw = Math.floor(width / 8);
  const bh = Math.floor(height / 8);
  const energy = new Float64Array(bw * bh);

  for (let by = 0; by < bh; by++) {
    for (let bx = 0; bx < bw; bx++) {
      let e = 0;
      for (let r = 0; r < 8; r++) {
        for (let c = 0; c < 8; c++) {
          const v = data[(by * 8 + r) * width + (bx * 8 + c)];
          e += v * v;
        }
      }
      energy[by * bw + bx] = e;
    }
  }

  // Expected tile period in blocks
  const expectedBlockPeriod = Math.floor(expectedTilePeriod / 8);

  // Search for the best period near the expected value
  let bestPeriod = expectedBlockPeriod;
  let bestCorr = -Infinity;
  let bestPhaseX = 0;
  let bestPhaseY = 0;

  for (let p = expectedBlockPeriod - searchRange; p <= expectedBlockPeriod + searchRange; p++) {
    if (p < 2) continue;

    // For each candidate phase offset
    for (let py = 0; py < p; py++) {
      for (let px = 0; px < p; px++) {
        let corr = 0;
        let count = 0;

        // Compute autocorrelation at this period and phase
        for (let by = py; by + p < bh; by += p) {
          for (let bx = px; bx + p < bw; bx += p) {
            const e1 = energy[by * bw + bx];
            const e2 = energy[(by + p) * bw + bx];
            const e3 = energy[by * bw + (bx + p)];
            corr += e1 * e2 + e1 * e3;
            count++;
          }
        }

        if (count > 0) {
          corr /= count;
          if (corr > bestCorr) {
            bestCorr = corr;
            bestPeriod = p;
            bestPhaseX = px;
            bestPhaseY = py;
          }
        }
      }
    }
  }

  // Convert from block coordinates back to subband pixels
  const tilePeriod = bestPeriod * 8;
  const phaseX = bestPhaseX * 8;
  const phaseY = bestPhaseY * 8;
  const tilesX = Math.floor((width - phaseX) / tilePeriod);
  const tilesY = Math.floor((height - phaseY) / tilePeriod);

  return {
    tilePeriod,
    phaseX,
    phaseY,
    tilesX,
    tilesY,
    totalTiles: tilesX * tilesY,
  };
}