Skip to content

Commit bd84609

Browse files
Shiloclaude
andcommitted
chore: the benchmark scripts follow the picture sizes, and one checks images with a fine texture
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e95d32f commit bd84609

5 files changed

Lines changed: 46 additions & 13 deletions

File tree

‎bench/.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ matrix.txt
88
review-*
99
committed/
1010
/codex-*
11+
codex-*
12+
review2-*

‎bench/alias.mjs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Images whose pixels repeat in a small pattern (dithered art, stippled transparency): does a
2+
// tile that draws them smaller show them as light as they are? Share of light pixels among the
3+
// solid ones, for the whole image and for pictures of it at the sizes a tile might have. 512 and
4+
// 256 are whole fractions of the image, where the same part of a texture is picked every time.
5+
import { pathToFileURL } from 'node:url';
6+
const B = await import(pathToFileURL(import.meta.dirname + '/../src/lib/bitify.js'));
7+
const w = 1024, h = 1024;
8+
const BAYER = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5];
9+
const make = pixel => {
10+
const data = new Uint8ClampedArray(w * h * 4);
11+
for (let y = 0, i = 0; y < h; y++) for (let x = 0; x < w; x++, i += 4) data.set(pixel(x, y), i);
12+
return B.analyze({ width: w, height: h, data });
13+
};
14+
const shade = (x, y) => 128 + 100 * Math.sin(x / 160) * Math.cos(y / 120);
15+
const cases = {
16+
// a smooth picture seen through a stipple: 2 by 2 blocks, solid and see-through in turn
17+
'stippled alpha': make((x, y) => { const v = shade(x, y); return [v, v, v, ((x >> 1) + (y >> 1)) % 2 ? 0 : 255]; }),
18+
// the same picture already dithered to black and white, as 1-bit art is
19+
'dithered source': make((x, y) => { const v = shade(x, y) / 255 > (BAYER[(y & 3) * 4 + (x & 3)] + 0.5) / 16 ? 255 : 0; return [v, v, v, 255]; }),
20+
// one-pixel stripes
21+
'striped source': make((x, y) => { const v = y % 2 ? 230 : 40; return [v, v, v, 255]; }),
22+
};
23+
const light = m => { let on = 0, solid = 0; for (const v of m) { solid += v > 0; on += v === 2; } return solid ? (100 * on / solid).toFixed(0).padStart(4) + '%' : ' -'; };
24+
const SIZES = [1024, 955, 700, 512, 478, 341, 256];
25+
console.log('image style ' + SIZES.map(s => (s === 1024 ? 'whole' : 'at ' + s).padStart(7)).join(''));
26+
for (const [name, img] of Object.entries(cases)) for (const style of ['solid', 'checker', 'bayer', 'atkinson', 'cutout']) {
27+
console.log(name.padEnd(17), style.padEnd(9), SIZES.map(s => light(B.mask(img, style, style === 'solid' || style === 'cutout' ? 128 : null, s, s)).padStart(7)).join(''));
28+
}
29+

‎bench/equiv.mjs‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,23 @@ for (let n = 0; n < 400; n++) {
2222
for (const st of STYLES) for (const t of [null, 1, 60, 128, 200, 254]) {
2323
const ma = O.mask(a, st, t), mb = N.mask(b, st, t);
2424
assert.deepEqual([...mb], [...ma], `${st} t=${t} ${w}x${h} n=${n}`);
25-
// the patterns and Atkinson are drawn afresh on the kept pixels, so only the others are the full mask sampled
26-
for (const k of ['cutout', 'lines', 'solid', 'silhouette'].includes(st) ? [2, 3, 5, 16, 50] : []) {
25+
// A smaller picture: in these four styles each of its pixels is the full mask's at the image pixel under its middle.
26+
// (The patterns and Atkinson are drawn afresh on the picture's pixels, so they are not.)
27+
for (const [mw, mh] of ['cutout', 'lines', 'solid', 'silhouette'].includes(st) ? [[w, h], [Math.ceil(w / 2), Math.ceil(h / 2)], [Math.ceil(w / 3), Math.ceil(h / 1.7)], [1, 1], [Math.max(1, w - 1), Math.max(1, h - 1)]] : []) {
2728
const want = [];
28-
for (let y = 0; y < h; y += k) for (let x = 0; x < w; x += k) want.push(ma[y * w + x]);
29-
assert.deepEqual([...N.mask(b, st, t, k)], want, `${st} t=${t} k=${k}`);
29+
for (let j = 0; j < mh; j++) for (let i = 0; i < mw; i++) want.push(ma[Math.floor((j + 0.5) * h / mh) * w + Math.floor((i + 0.5) * w / mw)]);
30+
assert.deepEqual([...N.mask(b, st, t, mw, mh)], want, `${st} t=${t} ${mw}x${mh}`);
3031
}
3132
checks++;
3233
}
3334
const m = O.mask(a, 'cutout');
3435
assert.deepEqual([...N.colorize(m, '#12ab9f', '#fe0180')], [...O.colorize(m, '#12ab9f', '#fe0180')]);
35-
const sh = N.shrink(src, 3), want = [];
36-
for (let y = 0; y < h; y += 3) for (let x = 0; x < w; x += 3) want.push(...data.subarray((y * w + x) * 4, (y * w + x) * 4 + 4));
36+
const sw = Math.ceil(w / 3), shh = Math.ceil(h / 2), sh = N.shrink(src, sw, shh), want = [];
37+
for (let j = 0; j < shh; j++) for (let i = 0; i < sw; i++) { const at = (Math.floor((j + 0.5) * h / shh) * w + Math.floor((i + 0.5) * w / sw)) * 4; want.push(...data.subarray(at, at + 4)); }
3738
assert.deepEqual([...sh], want);
3839
}
3940
// animations: unify
4041
const fr = [0, 1, 2].map(j => ({ width: 9, height: 7, data: Uint8ClampedArray.from({ length: 9 * 7 * 4 }, (_, i) => (i % 4 === 3 ? 255 : (i * (j + 3) * 37) % 256)) }));
4142
const ua = O.unify(fr.map(O.analyze)), ub = N.unify(fr.map(N.analyze));
4243
for (const f of ['auto', 'autoLine', 'autoTone', 'autoSeam', 'lo', 'hi']) assert.equal(ub[0][f], ua[0][f]);
43-
console.log(`identical: ${checks} image/style/threshold combinations, each also at 5 values of k`);
44+
console.log(`identical: ${checks} image/style/threshold combinations, each also as 5 smaller pictures`);

‎bench/kcost.mjs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Cost per converted pixel at different k, in nanoseconds.
1+
// Cost per converted pixel for the whole image and for smaller pictures of it, in nanoseconds.
22
import { pathToFileURL } from 'node:url';
33
const B = await import(pathToFileURL(import.meta.dirname + '/../src/lib/bitify.js'));
44
function photo(w, h) {
@@ -14,10 +14,10 @@ for (const [w, h] of [[2048, 2048], [4000, 3000]]) {
1414
const img = B.analyze(photo(w, h));
1515
for (const style of (process.argv[2] ?? 'cutout,lines,solid,bayer').split(',')) {
1616
const line = [`${w}x${h} ${style}`.padEnd(20)];
17-
for (const k of [1, 2, 3, 4, 8]) {
17+
for (const k of [1, 1.07, 2, 2.14, 4.19, 8]) {
1818
let best = Infinity, n;
19-
for (let i = 0; i < 12; i++) { const t = performance.now(); n = B.mask(img, style, 60 + i * 9, k).length; best = Math.min(best, performance.now() - t); }
20-
line.push(`k${k}: ${(best * 1e6 / n).toFixed(1)}ns (${best.toFixed(1)}ms)`);
19+
for (let i = 0; i < 12; i++) { const t = performance.now(); n = B.mask(img, style, 60 + i * 9, Math.round(w / k), Math.round(h / k)).length; best = Math.min(best, performance.now() - t); }
20+
line.push(`1 in ${k}: ${(best * 1e6 / n).toFixed(1)}ns (${best.toFixed(1)}ms)`);
2121
}
2222
console.log(line.join(' '));
2323
}

‎bench/tone.mjs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ const img = B.analyze({ width: w, height: h, data });
1111
const light = m => (100 * m.reduce((n, v) => n + (v === 2), 0) / m.length).toFixed(1).padStart(5);
1212
// how often a pixel has the same value as the one to its right: the texture, not just the tone
1313
const runs = (m, pw) => { let same = 0, n = 0; for (let p = 0; p + 1 < m.length; p++) if ((p + 1) % pw) { n++; same += m[p] === m[p + 1]; } return (100 * same / n).toFixed(1).padStart(5); };
14-
console.log('style ' + [1, 2, 3, 4, 5, 8, 11, 16].map(k => `k=${k}`.padStart(13)).join(''));
14+
const SIZES = [1200, 955, 600, 400, 300, 240, 150, 109];
15+
console.log('style ' + SIZES.map(s => `${s} wide`.padStart(12)).join(''));
1516
for (const style of ['solid', 'cutout', 'lines', 'checker', 'hatch', 'bayer', 'noise', 'atkinson']) {
16-
console.log(style.padEnd(10), [1, 2, 3, 4, 5, 8, 11, 16].map(k => { const m = B.mask(img, style, null, k); return `${light(m)}/${runs(m, Math.ceil(w / k))}`; }).join(' '));
17+
console.log(style.padEnd(10), SIZES.map(mw => { const mh = Math.round(h * mw / w), m = B.mask(img, style, null, mw, mh); return `${light(m)}/${runs(m, mw)}`; }).join(' '));
1718
}
1819
console.log('(each cell: % light / % of pixels equal to their right neighbour)');

0 commit comments

Comments
 (0)