ltmarx / core /tiling.ts
harelcain's picture
Upload 29 files
86323af verified
/**
* 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,
};
}