Spaces:
Running
Running
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,
};
}
|