From de41230b9b1e11e635e9557b71d538ff4c2e4468 Mon Sep 17 00:00:00 2001 From: Dmitriy Petrov Date: Tue, 11 Aug 2026 19:18:46 +0300 Subject: [PATCH] Improve OCR preprocessing and normalize merchant names --- src/parser.js | 2 +- src/vision.js | 28 +++++++++++++++++++++++++++- test/parser.test.js | 8 ++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/parser.js b/src/parser.js index c06f60f..60164d9 100644 --- a/src/parser.js +++ b/src/parser.js @@ -78,7 +78,7 @@ export function parseStoreHeaderText(text) { return { side: match[1].toLowerCase(), - merchant: match[2].trim(), + merchant: match[2].replace(/\s+/g, ""), rawText: normalized, }; } diff --git a/src/vision.js b/src/vision.js index fd6d37d..8e75eba 100644 --- a/src/vision.js +++ b/src/vision.js @@ -4,6 +4,16 @@ import { normalizeOcrText } from "./parser.js"; let cvPromise; let workerPromise; let progressListener = () => {}; +const WHITE_TEXT_MIN_CHANNEL = 150; +const WHITE_TEXT_MAX_SPREAD = 55; + +export function isWhiteTextPixel(red, green, blue, alpha = 255) { + return ( + alpha >= 128 && + Math.min(red, green, blue) >= WHITE_TEXT_MIN_CHANNEL && + Math.max(red, green, blue) - Math.min(red, green, blue) <= WHITE_TEXT_MAX_SPREAD + ); +} export function onOcrProgress(listener) { progressListener = listener; @@ -79,7 +89,6 @@ function prepareOcrCrop(sourceCanvas, rect) { const context = canvas.getContext("2d", { willReadFrequently: true }); context.imageSmoothingEnabled = false; - context.filter = "grayscale(1) contrast(2)"; context.drawImage( sourceCanvas, rect.x, @@ -92,6 +101,23 @@ function prepareOcrCrop(sourceCanvas, rect) { canvas.height, ); + const image = context.getImageData(0, 0, canvas.width, canvas.height); + for (let index = 0; index < image.data.length; index += 4) { + const value = isWhiteTextPixel( + image.data[index], + image.data[index + 1], + image.data[index + 2], + image.data[index + 3], + ) + ? 0 + : 255; + image.data[index] = value; + image.data[index + 1] = value; + image.data[index + 2] = value; + image.data[index + 3] = 255; + } + context.putImageData(image, 0, 0); + return canvas; } diff --git a/test/parser.test.js b/test/parser.test.js index 2e0783a..668d1c1 100644 --- a/test/parser.test.js +++ b/test/parser.test.js @@ -12,6 +12,13 @@ import { rectFromPoints, resolveFieldRect, } from "../src/parser.js"; +import { isWhiteTextPixel } from "../src/vision.js"; + +test("keeps white text pixels and removes colored or dark pixels", () => { + assert.equal(isWhiteTextPixel(220, 215, 195), true); + assert.equal(isWhiteTextPixel(210, 165, 80), false); + assert.equal(isWhiteTextPixel(120, 120, 120), false); +}); test("keeps only letters, digits and spaces in OCR item names", () => { assert.equal(cleanItemName(" ~~ Soulshot: S-grade ` "), "Soulshot S grade"); @@ -139,5 +146,6 @@ test("parses trade side and merchant from the store header", () => { merchant: "Dwa", rawText: "Private Store({Sell) - Dwa", }); + assert.equal(parseStoreHeaderText("Private Store(Buy) - Hik vision")?.merchant, "Hikvision"); assert.equal(parseStoreHeaderText("Items on Sale"), null); });