87 lines
3.3 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|