Files
genshin-assistant/src/lib/ocrPreprocess.test.ts
T
2026-07-07 22:02:24 +02:00

87 lines
3.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { binarizeForOcr, computeLuminanceHistogram, otsuThreshold, type Bitmap } from "./ocrPreprocess";
// Build a BGRA bitmap from a grid of [b,g,r] pixels.
function bitmapFrom(pixels: Array<[number, number, number]>, width: number, height: number): Bitmap {
const data = Buffer.alloc(width * height * 4);
pixels.forEach(([b, g, r], index) => {
data[index * 4] = b;
data[index * 4 + 1] = g;
data[index * 4 + 2] = r;
data[index * 4 + 3] = 255;
});
return { data, width, height };
}
describe("ocrPreprocess", () => {
it("computes a luminance histogram over all pixels", () => {
const bitmap = bitmapFrom([
[0, 0, 0],
[255, 255, 255],
[0, 0, 0],
[255, 255, 255],
], 2, 2);
const histogram = computeLuminanceHistogram(bitmap);
expect(histogram[0]).toBe(2);
expect(histogram[255]).toBe(2);
expect(histogram.reduce((sum, count) => sum + count, 0)).toBe(4);
});
it("otsu splits a clean bimodal image between the two peaks", () => {
const histogram = new Array<number>(256).fill(0);
histogram[20] = 50;
histogram[220] = 50;
const threshold = otsuThreshold(histogram);
expect(threshold).toBeGreaterThanOrEqual(20);
expect(threshold).toBeLessThan(220);
});
it("otsu is safe on an empty histogram", () => {
expect(otsuThreshold(new Array<number>(256).fill(0))).toBe(127);
});
it("can increase contrast before histogramming small numeric crops", () => {
const bitmap = bitmapFrom([
[118, 118, 118],
[138, 138, 138],
], 2, 1);
const normal = computeLuminanceHistogram(bitmap);
const contrasted = computeLuminanceHistogram(bitmap, { contrast: 80 });
const normalValues = normal.flatMap((count, value) => Array.from({ length: count }, () => value));
const contrastedValues = contrasted.flatMap((count, value) => Array.from({ length: count }, () => value));
expect(Math.max(...contrastedValues) - Math.min(...contrastedValues)).toBeGreaterThan(Math.max(...normalValues) - Math.min(...normalValues));
});
it("inverts bright foreground to black-on-white by default", () => {
// Bright text pixel + dark background pixel.
const bitmap = bitmapFrom([
[255, 255, 255], // bright -> should become black
[0, 0, 0], // dark -> should become white
], 2, 1);
const out = binarizeForOcr(bitmap, { threshold: 128 });
expect([out.data[0], out.data[1], out.data[2]]).toEqual([0, 0, 0]);
expect([out.data[4], out.data[5], out.data[6]]).toEqual([255, 255, 255]);
expect(out.data[3]).toBe(255);
});
it("keeps bright foreground white when inversion is disabled", () => {
const bitmap = bitmapFrom([
[255, 255, 255],
[0, 0, 0],
], 2, 1);
const out = binarizeForOcr(bitmap, { threshold: 128, invertBrightForeground: false });
expect(out.data[0]).toBe(255);
expect(out.data[4]).toBe(0);
});
it("preserves dimensions and always emits opaque pixels", () => {
const bitmap = bitmapFrom(Array.from({ length: 9 }, () => [100, 100, 100] as [number, number, number]), 3, 3);
const out = binarizeForOcr(bitmap);
expect(out.width).toBe(3);
expect(out.height).toBe(3);
for (let pixel = 0; pixel < 9; pixel++) {
expect(out.data[pixel * 4 + 3]).toBe(255);
}
});
});