2c6c1a8b31
Implements ADR-009 (structure + preprocessing; exact IK fixed coordinates still
need calibration against a reference 16:9 screenshot).
- src/lib/layoutProfile.ts: pure, unit-tested geometry for the artifact screen -
detail rect, the four detail crops, inventory rect/count crop, 5-col grid,
16:9 detection, aspect label, and an off-16:9 support warning. Single source of
truth; electron/main.ts now delegates all crop/grid geometry to it and keeps
colour detection only as the detail-rect fallback.
- src/lib/ocrPreprocess.ts: pure, unit-tested Otsu binarization with inversion
(artifact text is the bright foreground) over a BGRA bitmap.
- main.ts: OCR now reads an upscaled + binarized copy of each crop; the original
crop is retained for the diagnostics UI. CaptureResult carries layout info
{ aspect, isSixteenNine, warning }.
NOTE: image preprocessing changes the OCR input and cannot be validated by the
text-level eval harness; it needs a live Genshin 16:9 capture to confirm/tune
(threshold, invert, upscale factor). 88 tests + build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
75 lines
2.6 KiB
TypeScript
75 lines
2.6 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("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);
|
|
}
|
|
});
|
|
});
|