/** * Perceptual masking — adapt watermark strength based on local image content * * High-energy (textured) areas can tolerate stronger watermarks, * while smooth areas need weaker embedding to remain imperceptible. */ /** * Compute AC energy of an 8x8 DCT block (sum of squared AC coefficients) * Assumes the block is already in DCT domain. */ export function blockAcEnergy(dctBlock: Float64Array): number { let energy = 0; for (let i = 1; i < 64; i++) { // Skip DC (index 0) energy += dctBlock[i] * dctBlock[i]; } return energy; } /** * Compute perceptual masking factors for a set of DCT blocks * * Returns per-block multiplier for delta: * Δ_effective = Δ_base × masking_factor * * Factors are in [0.1, 2.0]: * - Smooth/flat blocks → factor near 0.1 (barely embed — changes would be visible) * - Textured/noisy blocks → factor up to 2.0 (can embed aggressively) * * Uses a square-root curve so that low-energy blocks are suppressed * more aggressively than a linear mapping would. * * @param blockEnergies - AC energy for each block * @returns Array of masking factors, same length as input */ export function computeMaskingFactors(blockEnergies: Float64Array): Float64Array { const n = blockEnergies.length; if (n === 0) return new Float64Array(0); // Compute median energy const sorted = new Float64Array(blockEnergies).sort(); const median = n % 2 === 0 ? (sorted[n / 2 - 1] + sorted[n / 2]) / 2 : sorted[Math.floor(n / 2)]; // Avoid division by zero const safeMedian = Math.max(median, 1e-6); const factors = new Float64Array(n); for (let i = 0; i < n; i++) { const ratio = blockEnergies[i] / safeMedian; // sqrt curve: suppresses low-energy blocks more aggressively // ratio=0 → 0, ratio=0.25 → 0.5, ratio=1 → 1, ratio=4 → 2 const curved = Math.sqrt(ratio); // Clamp to [0.1, 2.0] — flat areas barely embed factors[i] = Math.max(0.1, Math.min(2.0, curved)); } return factors; }