|
2 | 2 | * WebP lossless (VP8L) bitstream, as specified in RFC 9649 ("WebP Image Format") section 3 onwards, including its |
3 | 3 | * table of short distance codes. Copyright (c) 2024 IETF Trust and the persons identified as the document authors, |
4 | 4 | * Revised BSD License (see THIRD_PARTY_NOTICES.md). The encoder's choice of transforms follows libwebp's entropy |
5 | | - * analysis (src/enc/vp8l_enc.c, AnalyzeEntropy). Copyright (c) 2012 Google Inc., BSD-3-Clause License |
| 5 | + * analysis (src/enc/vp8l_enc.c, AnalyzeEntropy), its color transform search libwebp's (src/enc/predictor_enc.c). |
| 6 | + * Copyright (c) 2012 Google Inc., BSD-3-Clause License |
6 | 7 | */ |
7 | 8 |
|
8 | 9 | import { ByteWriter } from '../util/bytes.ts'; |
@@ -1228,6 +1229,167 @@ const BYTE_COST = Uint8Array.from({ length: 256 }, (_, c) => c < 128 ? c : 256 - |
1228 | 1229 | // cost estimate of a residual: sum of the channels' distances to zero |
1229 | 1230 | const residualCost = (r: number): number => BYTE_COST[r & 255] + BYTE_COST[r >>> 8 & 255] + BYTE_COST[r >>> 16 & 255] + BYTE_COST[r >>> 24]; |
1230 | 1231 |
|
| 1232 | +// the bits of the red and blue values of pixels (their histograms' entropy) |
| 1233 | +function redBlueBits(p: Uint32Array): number { |
| 1234 | + const red = new Uint32Array(256), blue = new Uint32Array(256); |
| 1235 | + for (let i = 0; i < p.length; i++) { |
| 1236 | + red[p[i] >> 16 & 255]++; |
| 1237 | + blue[p[i] & 255]++; |
| 1238 | + } |
| 1239 | + let bits = 0; |
| 1240 | + for (const h of [red, blue]) for (const n of h) if (n) bits += n * Math.log2(p.length / n); |
| 1241 | + return bits; |
| 1242 | +} |
| 1243 | + |
| 1244 | +const slog = (v: number): number => v ? v * Math.log2(v) : 0; |
| 1245 | +// (v log v up to 65536, built when first needed) |
| 1246 | +let SLOG: Float64Array | undefined; |
| 1247 | + |
| 1248 | +/** |
| 1249 | + * The bits a tile's histogram takes when added to the accumulated one (libwebp's CombinedShannonEntropy, in bits), |
| 1250 | + * with a bias favoring values near 0 (PredictionCostBias): over the n symbols the tile has (touched), with the |
| 1251 | + * accumulated histogram's sum and the sum of its entries' v log v |
| 1252 | + */ |
| 1253 | +function crossColorCost(accumulated: Uint32Array, accumulatedSlogs: Float64Array, accumulatedLogs: Float64Array, counts: Uint32Array, touched: Int32Array, n: number, accumulatedSum: number, accumulatedSlog: number): number { |
| 1254 | + const table = SLOG!; |
| 1255 | + let sum = accumulatedSlog, sumX = 0; |
| 1256 | + for (let k = 0; k < n; k++) { |
| 1257 | + const i = touched[k], x = counts[i], y = accumulated[i]; |
| 1258 | + sumX += x; |
| 1259 | + // ((x + y) log (x + y) - y log y = x log y + (x + y) log(1 + x / y), the latter's series exact enough for y |
| 1260 | + // beyond the table, where x / y is at most 1/255) |
| 1261 | + if (x + y < table.length) sum += table[x] + table[x + y] - accumulatedSlogs[i]; |
| 1262 | + else { |
| 1263 | + const t = x / y; |
| 1264 | + sum += table[x] + x * accumulatedLogs[i] + (x + y) * (t - t * t / 2 + t * t * t / 3) * Math.LOG2E; |
| 1265 | + } |
| 1266 | + } |
| 1267 | + let bias = 3 * counts[0], weight = 240; |
| 1268 | + for (let i = 1; i < 16; i++) { |
| 1269 | + bias += weight * (counts[i] + counts[256 - i]) / 100; |
| 1270 | + weight *= 0.6; |
| 1271 | + } |
| 1272 | + return slog(sumX) + slog(sumX + accumulatedSum) - sum - bias / 10; |
| 1273 | +} |
| 1274 | + |
| 1275 | +/** |
| 1276 | + * libwebp's color transform search (src/enc/predictor_enc.c, VP8LColorSpaceTransform): per block of 2^bits pixels the |
| 1277 | + * multipliers (3.5 bit fixed point) of green for red, and of green and red for blue, whose residuals take the fewest |
| 1278 | + * bits with the blocks' before (blocks keeping their neighbors' multipliers or 0 favored), applied to the pixels; |
| 1279 | + * returns the sub-image of the multipliers. Coarser than libwebp's: the costs are of every fourth pixel and the search |
| 1280 | + * for blue's multipliers stops at steps of 4, which compresses about as well (0.1% larger) at a third of the time |
| 1281 | + */ |
| 1282 | +function colorTransform(p: Uint32Array, width: number, height: number, bits: number): Uint32Array { |
| 1283 | + const size = 1 << bits, blocksX = subSize(width, bits), blocksY = subSize(height, bits); |
| 1284 | + const image = new Uint32Array(blocksX * blocksY); |
| 1285 | + const accumulatedRed = new Uint32Array(256), accumulatedBlue = new Uint32Array(256), histo = new Uint32Array(256), touched = new Int32Array(256); |
| 1286 | + // (a tile's green and red as signed bytes, red and blue as they are) |
| 1287 | + const green = new Int32Array(size * size), red = new Int32Array(size * size), redByte = new Int32Array(size * size), blue = new Int32Array(size * size); |
| 1288 | + const s8 = (v: number): number => v << 24 >> 24; |
| 1289 | + // (an accumulated histogram's sum, its entries' v log v and logs, and the sum of the former) |
| 1290 | + SLOG ??= Float64Array.from({ length: 65536 }, (_, v) => slog(v)); |
| 1291 | + const redSlogs = new Float64Array(256), blueSlogs = new Float64Array(256), redLogs = new Float64Array(256), blueLogs = new Float64Array(256); |
| 1292 | + const sums = (h: Uint32Array, slogs: Float64Array, logs: Float64Array): [number, number] => { |
| 1293 | + let total = 0, entropy = 0; |
| 1294 | + for (let i = 0; i < 256; i++) { |
| 1295 | + total += h[i]; |
| 1296 | + entropy += slogs[i] = slog(h[i]); |
| 1297 | + logs[i] = h[i] ? Math.log2(h[i]) : 0; |
| 1298 | + } |
| 1299 | + return [total, entropy]; |
| 1300 | + }; |
| 1301 | + let prevX = [0, 0, 0], prevY = [0, 0, 0]; |
| 1302 | + for (let by = 0; by < blocksY; by++) { |
| 1303 | + for (let bx = 0; bx < blocksX; bx++) { |
| 1304 | + const x0 = bx * size, y0 = by * size, x1 = Math.min(width, x0 + size), y1 = Math.min(height, y0 + size); |
| 1305 | + if (by) { |
| 1306 | + const above = image[(by - 1) * blocksX + bx]; |
| 1307 | + prevY = [above & 255, above >> 8 & 255, above >> 16 & 255]; |
| 1308 | + } |
| 1309 | + // (the pixels the costs are of: every fourth, shifted by one each row) |
| 1310 | + let count = 0; |
| 1311 | + for (let y = y0; y < y1; y++) |
| 1312 | + for (let i = y * width + x0 + (y & 3), end = y * width + x1; i < end; i += 4, count++) { |
| 1313 | + const v = p[i]; |
| 1314 | + green[count] = s8(v >> 8); |
| 1315 | + red[count] = s8(v >> 16); |
| 1316 | + redByte[count] = v >> 16 & 255; |
| 1317 | + blue[count] = v & 255; |
| 1318 | + } |
| 1319 | + // (green to red: by halving steps from 32) |
| 1320 | + const [redSum, redSlog] = sums(accumulatedRed, redSlogs, redLogs); |
| 1321 | + const redCost = (gtr: number): number => { |
| 1322 | + const m = s8(gtr); |
| 1323 | + let n = 0; |
| 1324 | + for (let k = 0; k < count; k++) { |
| 1325 | + const v = redByte[k] - (m * green[k] >> 5) & 255; |
| 1326 | + if (!histo[v]++) touched[n++] = v; |
| 1327 | + } |
| 1328 | + let cost = crossColorCost(accumulatedRed, redSlogs, redLogs, histo, touched, n, redSum, redSlog); |
| 1329 | + for (let k = 0; k < n; k++) histo[touched[k]] = 0; |
| 1330 | + if ((gtr & 255) === prevX[0]) cost -= 3; |
| 1331 | + if ((gtr & 255) === prevY[0]) cost -= 3; |
| 1332 | + if (gtr === 0) cost -= 3; |
| 1333 | + return cost; |
| 1334 | + }; |
| 1335 | + let gtr = 0, best = redCost(0); |
| 1336 | + for (let iter = 0; iter < 6; iter++) { |
| 1337 | + const step = 32 >> iter; |
| 1338 | + for (const offset of [-step, step]) { |
| 1339 | + const cost = redCost(gtr + offset); |
| 1340 | + if (cost < best) [best, gtr] = [cost, gtr + offset]; |
| 1341 | + } |
| 1342 | + } |
| 1343 | + // (green and red to blue: along the axes and diagonals) |
| 1344 | + const [blueSum, blueSlog] = sums(accumulatedBlue, blueSlogs, blueLogs); |
| 1345 | + const blueCost = (gtb: number, rtb: number): number => { |
| 1346 | + const mg = s8(gtb), mr = s8(rtb); |
| 1347 | + let n = 0; |
| 1348 | + for (let k = 0; k < count; k++) { |
| 1349 | + const v = blue[k] - (mg * green[k] >> 5) - (mr * red[k] >> 5) & 255; |
| 1350 | + if (!histo[v]++) touched[n++] = v; |
| 1351 | + } |
| 1352 | + let cost = crossColorCost(accumulatedBlue, blueSlogs, blueLogs, histo, touched, n, blueSum, blueSlog); |
| 1353 | + for (let k = 0; k < n; k++) histo[touched[k]] = 0; |
| 1354 | + if ((gtb & 255) === prevX[1]) cost -= 3; |
| 1355 | + if ((gtb & 255) === prevY[1]) cost -= 3; |
| 1356 | + if ((rtb & 255) === prevX[2]) cost -= 3; |
| 1357 | + if ((rtb & 255) === prevY[2]) cost -= 3; |
| 1358 | + if (gtb === 0) cost -= 3; |
| 1359 | + if (rtb === 0) cost -= 3; |
| 1360 | + return cost; |
| 1361 | + }; |
| 1362 | + let gtb = 0, rtb = 0; |
| 1363 | + best = blueCost(0, 0); |
| 1364 | + for (const step of [16, 8, 4]) { |
| 1365 | + for (const [dg, dr] of [[0, -1], [0, 1], [-1, 0], [1, 0], [-1, -1], [-1, 1], [1, -1], [1, 1]]) { |
| 1366 | + const cost = blueCost(gtb + dg * step, rtb + dr * step); |
| 1367 | + if (cost < best) [best, gtb, rtb] = [cost, gtb + dg * step, rtb + dr * step]; |
| 1368 | + } |
| 1369 | + } |
| 1370 | + prevX = [gtr & 255, gtb & 255, rtb & 255]; |
| 1371 | + image[by * blocksX + bx] = (0xff000000 | prevX[2] << 16 | prevX[1] << 8 | prevX[0]) >>> 0; |
| 1372 | + // the block transformed, its pixels accumulated (but repeated ones: backward references take them) |
| 1373 | + const mgr = s8(gtr), mgb = s8(gtb), mrb = s8(rtb); |
| 1374 | + for (let y = y0; y < y1; y++) |
| 1375 | + for (let i = y * width + x0, end = y * width + x1; i < end; i++) { |
| 1376 | + const v = p[i], g = s8(v >> 8), rs = s8(v >> 16); |
| 1377 | + const r = (v >> 16) - (mgr * g >> 5) & 255, b = v - (mgb * g >> 5) - (mrb * rs >> 5) & 255; |
| 1378 | + p[i] = (v & 0xff00ff00 | r << 16 | b) >>> 0; |
| 1379 | + } |
| 1380 | + for (let y = y0; y < y1; y++) |
| 1381 | + for (let i = y * width + x0, end = y * width + x1; i < end; i++) { |
| 1382 | + const v = p[i]; |
| 1383 | + if (i >= 2 && v === p[i - 2] && v === p[i - 1]) continue; |
| 1384 | + if (i >= width + 2 && p[i - 2] === p[i - width - 2] && p[i - 1] === p[i - width - 1] && v === p[i - width]) continue; |
| 1385 | + accumulatedRed[v >> 16 & 255]++; |
| 1386 | + accumulatedBlue[v & 255]++; |
| 1387 | + } |
| 1388 | + } |
| 1389 | + } |
| 1390 | + return image; |
| 1391 | +} |
| 1392 | + |
1231 | 1393 | /** |
1232 | 1394 | * libwebp's entropy analysis (AnalyzeEntropy): which of the pixels and their differences to the pixel left of them, |
1233 | 1395 | * each with green subtracted from red and blue or not, take the fewest bits (the predictor with the cost of its |
@@ -1414,6 +1576,20 @@ async function encodeWith(argb: Uint32Array, width: number, height: number, colo |
1414 | 1576 | w.write(bits - 2, 3); |
1415 | 1577 | const modes = await applyPredictor(pixels, width, height, bits, threads); |
1416 | 1578 | await writeImage(w, modes, subSize(width, bits), false); |
| 1579 | + // (the residuals' red and blue predicted from their green, like libwebp does with the predictor; not if no block |
| 1580 | + // gains from it, all multipliers 0 leave the pixels as they are) |
| 1581 | + if (redBlue) { |
| 1582 | + const colorBits = 5, before = pixels.slice(), multipliers = colorTransform(pixels, width, height, colorBits); |
| 1583 | + // (nor if red and blue don't take fewer bits in all, with about 12 bits per block that changes them) |
| 1584 | + const changed = multipliers.reduce((n, m) => n + (m !== 0xff000000 ? 1 : 0), 0); |
| 1585 | + if (changed && redBlueBits(pixels) + 12 * changed >= redBlueBits(before)) pixels.set(before); |
| 1586 | + else if (changed) { |
| 1587 | + w.write(1, 1); |
| 1588 | + w.write(Transform.COLOR, 2); |
| 1589 | + w.write(colorBits - 2, 3); |
| 1590 | + await writeImage(w, multipliers, subSize(width, colorBits), false); |
| 1591 | + } |
| 1592 | + } |
1417 | 1593 | } |
1418 | 1594 | } |
1419 | 1595 |
|
|
0 commit comments