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