Spaces:
Running
Running
File size: 6,642 Bytes
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 185 186 187 188 | /**
* High-level watermark embedder
*
* Takes a Y plane + payload + key + config → returns watermarked Y plane
*/
import type { WatermarkConfig, EmbedResult, Buffer2D } from './types.js';
import { yPlaneToBuffer, bufferToYPlane, dwtForward, dwtInverse, extractSubband, writeSubband } from './dwt.js';
import { dctForward8x8, dctInverse8x8, extractBlock, writeBlock, ZIGZAG_ORDER } from './dct.js';
import { dmqimEmbed } from './dmqim.js';
import { crcAppend } from './crc.js';
import { BchCodec } from './bch.js';
import { generateDithers, generatePermutation } from './keygen.js';
import { computeTileGrid, getTileOrigin, getTileBlocks } from './tiling.js';
import { blockAcEnergy, computeMaskingFactors } from './masking.js';
/**
* Convert a 32-bit payload (4 bytes) to a bit array
*/
export function payloadToBits(payload: Uint8Array): Uint8Array {
const bits = new Uint8Array(payload.length * 8);
for (let i = 0; i < payload.length; i++) {
for (let b = 0; b < 8; b++) {
bits[i * 8 + b] = (payload[i] >> (7 - b)) & 1;
}
}
return bits;
}
/**
* Convert a bit array back to bytes
*/
export function bitsToPayload(bits: Uint8Array): Uint8Array {
const bytes = new Uint8Array(Math.ceil(bits.length / 8));
for (let i = 0; i < bits.length; i++) {
if (bits[i]) {
bytes[Math.floor(i / 8)] |= 1 << (7 - (i % 8));
}
}
return bytes;
}
/**
* Embed a watermark into a Y plane
*/
export function embedWatermark(
yPlane: Uint8Array,
width: number,
height: number,
payload: Uint8Array,
key: string,
config: WatermarkConfig
): EmbedResult {
// Step 1: Encode payload
// Always use exactly 32 bits of payload, zero-pad the BCH message to fill k
const PAYLOAD_BITS = 32;
let payloadBits = payloadToBits(payload);
// Truncate or pad to exactly 32 bits
if (payloadBits.length < PAYLOAD_BITS) {
const padded = new Uint8Array(PAYLOAD_BITS);
padded.set(payloadBits);
payloadBits = padded;
} else if (payloadBits.length > PAYLOAD_BITS) {
payloadBits = payloadBits.subarray(0, PAYLOAD_BITS);
}
// CRC protects the 32-bit payload
const withCrc = crcAppend(payloadBits, config.crc.bits);
// Pad to fill BCH message length k
const bchMessage = new Uint8Array(config.bch.k);
bchMessage.set(withCrc);
const bch = new BchCodec(config.bch);
const encoded = bch.encode(bchMessage);
const codedLength = encoded.length;
// Generate interleaving permutation
const perm = generatePermutation(key, codedLength);
const interleaved = new Uint8Array(codedLength);
for (let i = 0; i < codedLength; i++) {
interleaved[perm[i]] = encoded[i];
}
// Step 2: Forward DWT
const buf = yPlaneToBuffer(yPlane, width, height);
const { buf: dwtBuf, dims } = dwtForward(buf, config.dwtLevels);
// Step 3: Extract HL subband at deepest level
// After N levels, the HL subband is in the bottom-left of the level-N region
let w = dims[dims.length - 1].w;
let h = dims[dims.length - 1].h;
for (let l = 0; l < config.dwtLevels - 1; l++) {
w >>= 1;
h >>= 1;
}
const hlSubband = extractSubband(dwtBuf, dims[dims.length - 1].w, dims[dims.length - 1].h, 'HL');
// Step 4: Set up tile grid
// tilePeriod is in spatial pixels; in subband it's tilePeriod / (2^levels)
const subbandTilePeriod = Math.floor(config.tilePeriod / (1 << config.dwtLevels));
const tileGrid = computeTileGrid(hlSubband.width, hlSubband.height, subbandTilePeriod);
if (tileGrid.totalTiles === 0) {
// Frame too small for any tiles — return unchanged
return { yPlane: new Uint8Array(yPlane), psnr: Infinity };
}
// Step 5: Generate dithers — same sequence reused per tile so each tile
// is independently decodable (required for crop robustness)
const coeffsPerBlock = config.zigzagPositions.length;
const maxCoeffsPerTile = 1024; // Upper bound
const dithers = generateDithers(key, maxCoeffsPerTile, config.delta);
// Step 6: For each tile, embed the coded bits
// Reusable block buffer to avoid allocation per block
const blockBuf = new Float64Array(64);
// Precompute zigzag → coefficient index mapping
const zigCoeffIdx = new Int32Array(config.zigzagPositions.length);
for (let z = 0; z < config.zigzagPositions.length; z++) {
const [r, c] = ZIGZAG_ORDER[config.zigzagPositions[z]];
zigCoeffIdx[z] = r * 8 + c;
}
for (let tileIdx = 0; tileIdx < tileGrid.totalTiles; tileIdx++) {
let ditherIdx = 0; // Reset per tile — each tile uses same dither sequence
const origin = getTileOrigin(tileGrid, tileIdx);
const blocks = getTileBlocks(origin.x, origin.y, subbandTilePeriod, hlSubband.width, hlSubband.height);
// Compute masking factors if enabled
let maskingFactors: Float64Array | null = null;
if (config.perceptualMasking && blocks.length > 0) {
const energies = new Float64Array(blocks.length);
for (let bi = 0; bi < blocks.length; bi++) {
extractBlock(hlSubband.data, hlSubband.width, blocks[bi].row, blocks[bi].col, blockBuf);
dctForward8x8(blockBuf);
energies[bi] = blockAcEnergy(blockBuf);
}
maskingFactors = computeMaskingFactors(energies);
}
let bitIdx = 0;
for (let bi = 0; bi < blocks.length; bi++) {
const { row, col } = blocks[bi];
extractBlock(hlSubband.data, hlSubband.width, row, col, blockBuf);
// Forward DCT
dctForward8x8(blockBuf);
const maskFactor = maskingFactors ? maskingFactors[bi] : 1.0;
const effectiveDelta = config.delta * maskFactor;
// Embed bits into selected zigzag positions
for (let z = 0; z < zigCoeffIdx.length; z++) {
if (bitIdx >= codedLength) bitIdx = 0; // Repeat pattern
const coeffIdx = zigCoeffIdx[z];
const bit = interleaved[bitIdx];
const dither = dithers[ditherIdx++];
blockBuf[coeffIdx] = dmqimEmbed(blockBuf[coeffIdx], bit, effectiveDelta, dither);
bitIdx++;
}
// Inverse DCT and write back
dctInverse8x8(blockBuf);
writeBlock(hlSubband.data, hlSubband.width, row, col, blockBuf);
}
}
// Step 7: Write modified HL subband back and inverse DWT
writeSubband(dwtBuf, dims[dims.length - 1].w, dims[dims.length - 1].h, 'HL', hlSubband);
dwtInverse(dwtBuf, dims);
// Convert back to Y plane
const watermarkedY = bufferToYPlane(dwtBuf);
// Compute PSNR
let mse = 0;
for (let i = 0; i < yPlane.length; i++) {
const diff = yPlane[i] - watermarkedY[i];
mse += diff * diff;
}
mse /= yPlane.length;
const psnr = mse > 0 ? 10 * Math.log10(255 * 255 / mse) : Infinity;
return { yPlane: watermarkedY, psnr };
}
|