Spaces:
Running
Running
File size: 10,055 Bytes
2dc8d66 | 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 | // AI-generated content marking for the images this page makes (EU AI Act Art. 50(2)), the same marks as the
// Python packages (agate/marking.py of Logolabs/agate-preview-002 / -003):
// * an invisible watermark in the pixels: invisible-watermark's (MIT) 'dwtDctSvd' method, fixed 64-bit payload
// "AGATE" + release ("AGATE001" / "AGATE002" / "AGATE003"), ported from the library with its exact conventions
// (OpenCV 8-bit YUV, U channel only, Haar LL band, 4x4 DCT + SVD, s0 quantised to 36, the library's swapped
// detail order in the inverse DWT, numpy's truncating uint8 cast). agate.detect_watermark() in Python reads it.
// * provenance text chunks (tEXt) in the downloaded PNG: ai_generated, generator, model, watermark -- the keys
// and values the Python packages write. The prompt is NOT written.
// Neither mark is tamper-proof; see the model cards.
export const METHOD = "dwtDctSvd";
const SCALE = 36, BLOCK = 4;
const S2 = 0.7071067811865476; // PyWavelets' Haar tap, 1/sqrt(2)
export function marks(release) {
const payload = "AGATE" + release; // 8 ASCII bytes = 64 bits
return {
payload,
info: {
ai_generated: "true",
generator: `Agate Preview ${release} (LogoLabs)`,
model: `Logolabs/agate-preview-${release}`,
watermark: `invisible-watermark ${METHOD}, payload ${payload}`,
},
};
}
const desc = (x) => (x + 8192) >> 14; // OpenCV CV_DESCALE(x, 14)
const sat = (x) => (x < 0 ? 0 : x > 255 ? 255 : x);
const u8unsafe = (x) => { const t = Math.trunc(x) % 256; return t < 0 ? t + 256 : t; }; // numpy float -> uint8
// orthonormal 4-point DCT-II matrix (cv2.dct on a 4x4 block = C X C^T)
const C = [];
for (let k = 0; k < BLOCK; k++) {
C.push([]);
for (let n = 0; n < BLOCK; n++) C[k].push(Math.sqrt(k === 0 ? 1 / BLOCK : 2 / BLOCK) * Math.cos(Math.PI * (2 * n + 1) * k / (2 * BLOCK)));
}
function mul(A, B) { const R = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]; for (let i = 0; i < 4; i++) for (let j = 0; j < 4; j++) { let s = 0; for (let k = 0; k < 4; k++) s += A[i][k] * B[k][j]; R[i][j] = s; } return R; }
const T = (A) => A[0].map((_, j) => A.map((r) => r[j]));
const CT = T(C);
// largest singular value of a 4x4 matrix and its singular vectors (Jacobi on D^T D)
function topSVD(D) {
const M = mul(T(D), D);
const V = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]];
for (let sweep = 0; sweep < 30; sweep++) {
let off = 0;
for (let p = 0; p < 4; p++) for (let q = p + 1; q < 4; q++) off += M[p][q] * M[p][q];
if (off < 1e-30) break;
for (let p = 0; p < 4; p++) for (let q = p + 1; q < 4; q++) {
if (Math.abs(M[p][q]) < 1e-300) continue;
const th = (M[q][q] - M[p][p]) / (2 * M[p][q]);
const t = Math.sign(th || 1) / (Math.abs(th) + Math.sqrt(th * th + 1));
const c = 1 / Math.sqrt(t * t + 1), s = t * c;
for (let k = 0; k < 4; k++) { const a = M[k][p], b = M[k][q]; M[k][p] = c * a - s * b; M[k][q] = s * a + c * b; }
for (let k = 0; k < 4; k++) { const a = M[p][k], b = M[q][k]; M[p][k] = c * a - s * b; M[q][k] = s * a + c * b; }
for (let k = 0; k < 4; k++) { const a = V[k][p], b = V[k][q]; V[k][p] = c * a - s * b; V[k][q] = s * a + c * b; }
}
}
let best = 0;
for (let i = 1; i < 4; i++) if (M[i][i] > M[best][best]) best = i;
const s0 = Math.sqrt(Math.max(0, M[best][best]));
let v = V.map((r) => r[best]), u;
if (s0 < 1e-12) { u = [1, 0, 0, 0]; v = [1, 0, 0, 0]; }
else u = D.map((r) => (r[0] * v[0] + r[1] * v[1] + r[2] * v[2] + r[3] * v[3]) / s0);
return { s0, u, v };
}
function payloadBits(payload) {
const bytes = new TextEncoder().encode(payload), bits = [];
for (const b of bytes) for (let k = 7; k >= 0; k--) bits.push((b >> k) & 1);
return bits;
}
// U channel (OpenCV BGR2YUV, 8 bit) of an RGBA buffer -> Int32Array(W*H), plus Y and V for the inverse
function toYUV(rgba, W, H) {
const Y = new Int32Array(W * H), U = new Int32Array(W * H), V = new Int32Array(W * H);
for (let p = 0; p < W * H; p++) {
const r = rgba[4 * p], g = rgba[4 * p + 1], b = rgba[4 * p + 2];
const y = desc(b * 1868 + g * 9617 + r * 4899);
Y[p] = y; U[p] = sat(desc((b - y) * 8061 + (128 << 14))); V[p] = sat(desc((r - y) * 14369 + (128 << 14)));
}
return { Y, U, V };
}
// LL band of the Haar DWT of U over the (H//4*4, W//4*4) region; -> {ca, ch, cv, cd} as Float64Array (h2 x w2)
function haar(U, W, R, Cc) {
const h2 = R / 2, w2 = Cc / 2, n = h2 * w2;
const ca = new Float64Array(n), ch = new Float64Array(n), cv = new Float64Array(n), cd = new Float64Array(n);
for (let i = 0; i < h2; i++) for (let j = 0; j < w2; j++) {
const a = U[(2 * i) * W + 2 * j], b = U[(2 * i) * W + 2 * j + 1], c = U[(2 * i + 1) * W + 2 * j], d = U[(2 * i + 1) * W + 2 * j + 1];
const k = i * w2 + j;
// PyWavelets' exact float order: pairs along rows first (a|c, b|d), then along columns
const l0 = a * S2 + c * S2, l1 = b * S2 + d * S2, h0 = a * S2 - c * S2, h1 = b * S2 - d * S2;
ca[k] = l0 * S2 + l1 * S2; cv[k] = l0 * S2 - l1 * S2; ch[k] = h0 * S2 + h1 * S2; cd[k] = h0 * S2 - h1 * S2;
}
return { ca, ch, cv, cd, h2, w2 };
}
function blockAt(ca, w2, bi, bj) { const B = []; for (let r = 0; r < 4; r++) { B.push([]); for (let c = 0; c < 4; c++) B[r].push(ca[(bi * 4 + r) * w2 + bj * 4 + c]); } return B; }
// Embed `payload` in place into rgba (Uint8ClampedArray / Uint8Array, W*H*4). Needs W*H >= 256*256.
export function embedWatermark(rgba, W, H, payload) {
if (W * H < 256 * 256) throw new Error("watermark: image too small (needs at least 256 x 256)");
const bits = payloadBits(payload);
const { Y, U, V } = toYUV(rgba, W, H);
const R = Math.floor(H / 4) * 4, Cc = Math.floor(W / 4) * 4;
const { ca, ch, cv, cd, h2, w2 } = haar(U, W, R, Cc);
const nbi = Math.floor(h2 / 4), nbj = Math.floor(w2 / 4);
let num = 0;
for (let bi = 0; bi < nbi; bi++) for (let bj = 0; bj < nbj; bj++, num++) {
const D = mul(mul(C, blockAt(ca, w2, bi, bj)), CT);
const { s0, u, v } = topSVD(D);
const s1 = (Math.floor(s0 / SCALE) + 0.25 + 0.5 * bits[num % bits.length]) * SCALE;
const Dn = D.map((row, i) => row.map((x, j) => x + (s1 - s0) * u[i] * v[j]));
const Bn = mul(mul(CT, Dn), C);
for (let r = 0; r < 4; r++) for (let c = 0; c < 4; c++) ca[(bi * 4 + r) * w2 + bj * 4 + c] = Bn[r][c];
}
// inverse Haar with the library's swapped details (cv as H, ch as V), truncating uint8 cast
for (let i = 0; i < h2; i++) for (let j = 0; j < w2; j++) {
// pywt.idwt2((ca, (cv, ch, cd))): the library's swapped details; PyWavelets' float order (columns, then rows)
const k = i * w2 + j, A = ca[k], hIn = cv[k], vIn = ch[k], d = cd[k];
const L0 = A * S2 + vIn * S2, L1 = A * S2 - vIn * S2, H0 = hIn * S2 + d * S2, H1 = hIn * S2 - d * S2;
U[(2 * i) * W + 2 * j] = u8unsafe(L0 * S2 + H0 * S2);
U[(2 * i) * W + 2 * j + 1] = u8unsafe(L1 * S2 + H1 * S2);
U[(2 * i + 1) * W + 2 * j] = u8unsafe(L0 * S2 - H0 * S2);
U[(2 * i + 1) * W + 2 * j + 1] = u8unsafe(L1 * S2 - H1 * S2);
}
for (let p = 0; p < W * H; p++) { // OpenCV YUV2BGR, 8 bit
const y = Y[p], uu = U[p] - 128, vq = V[p] - 128;
rgba[4 * p + 2] = sat(y + desc(uu * 33292));
rgba[4 * p + 1] = sat(y + desc(uu * -6472 + vq * -9519));
rgba[4 * p] = sat(y + desc(vq * 18678));
}
return rgba;
}
// -> {bits, text, bitAccuracy} against `payload` (a self-check; the reference detector is the Python one)
export function readWatermark(rgba, W, H, payload) {
const want = payloadBits(payload), n = want.length;
const { U } = toYUV(rgba, W, H);
const R = Math.floor(H / 4) * 4, Cc = Math.floor(W / 4) * 4;
const { ca, h2, w2 } = haar(U, W, R, Cc);
const nbi = Math.floor(h2 / 4), nbj = Math.floor(w2 / 4);
const sum = new Float64Array(n), cnt = new Float64Array(n);
let num = 0;
for (let bi = 0; bi < nbi; bi++) for (let bj = 0; bj < nbj; bj++, num++) {
const { s0 } = topSVD(mul(mul(C, blockAt(ca, w2, bi, bj)), CT));
sum[num % n] += (s0 % SCALE) > SCALE * 0.5 ? 1 : 0; cnt[num % n] += 1;
}
const bits = Array.from(sum, (s, k) => (s / cnt[k]) * 255 > 127 ? 1 : 0);
let same = 0; bits.forEach((b, k) => { if (b === want[k]) same++; });
const bytes = []; for (let k = 0; k < n; k += 8) { let b = 0; for (let q = 0; q < 8; q++) b = (b << 1) | bits[k + q]; bytes.push(b); }
return { bits, text: String.fromCharCode(...bytes), bitAccuracy: same / n };
}
// ---- PNG tEXt chunks ---------------------------------------------------------------------------
let CRC_TABLE = null;
function crc32(bytes) {
if (!CRC_TABLE) { CRC_TABLE = new Uint32Array(256); for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; CRC_TABLE[n] = c >>> 0; } }
let c = 0xffffffff;
for (const b of bytes) c = CRC_TABLE[(c ^ b) & 0xff] ^ (c >>> 8);
return (c ^ 0xffffffff) >>> 0;
}
// PNG bytes -> PNG bytes with one tEXt chunk per entry of `info` (Latin-1 keys/values), inserted before IEND.
export function addPngText(png, info) {
const latin1 = (s) => Uint8Array.from(s, (ch) => { const c = ch.charCodeAt(0); return c < 256 ? c : 63; });
const chunks = [];
for (const [k, v] of Object.entries(info)) {
const data = new Uint8Array([...latin1(k), 0, ...latin1(String(v))]);
const typeData = new Uint8Array([116, 69, 88, 116, ...data]); // "tEXt"
const out = new Uint8Array(12 + data.length), dv = new DataView(out.buffer);
dv.setUint32(0, data.length); out.set(typeData, 4); dv.setUint32(8 + data.length, crc32(typeData));
chunks.push(out);
}
// find IEND (the last chunk): 12 bytes from the end in every well-formed PNG
const iend = png.length - 12;
const extra = chunks.reduce((s, c) => s + c.length, 0);
const res = new Uint8Array(png.length + extra);
res.set(png.subarray(0, iend), 0);
let o = iend;
for (const c of chunks) { res.set(c, o); o += c.length; }
res.set(png.subarray(iend), o);
return res;
}
|