发布于 2026-04-08
Tesseract.js 提供了一套轻量级的 JavaScript OCR 解决方案,兼容浏览器与 Node.js 环境。本文将结合项目的工程实践,阐述如何通过本地语言包、图像预处理与任务队列机制,在 Electron 客户端构建完整的离线图像文字识别模块。
Tesseract.js 最简单的用法是直接调用其提供的快捷方法,这适用于一次性的简单识别任务:
const { recognize } = require('tesseract.js');
async function quickRecognize() {
const result = await recognize(
'image.png',
'eng',
{ logger: m => console.log(m) }
);
console.log(result.data.text);
}
这种方式虽然简单,但每次调用都会重新下载或读取模型文件并初始化引擎,性能开销较大,不适合频繁或大批量的识别需求。
在实际生产环境中,我们通常会通过创建和维护 Worker 来实现引擎的复用,从而大幅提升后续识别任务的处理速度。Worker 是一种维护引擎生命周期的对象。
const { createWorker } = require('tesseract.js');
async function advancedRecognize(imagePath) {
// 创建并初始化 Worker
const worker = await createWorker('eng', 1, {
logger: m => console.log(m),
});
// 执行识别,可以重复调用
const result = await worker.recognize(imagePath);
console.log(result.data.text);
// 任务全部完成后释放资源
await worker.terminate();
}
如果需要同时识别多种语言,可以在初始化 Worker 时通过加号连接语言代码。如这里将多个语言代码(如 eng 和 chi_sim)通过 + 符号连接,使引擎同时加载多语种模型。
const worker = await createWorker('eng+chi_sim');
在制作本地客户端时,项目需要支持完全离线运行,所有的语言包数据都通过本地文件系统提供,而不是动态下载。在创建 Worker 时,通过配置项指定了语言包的本地路径,langPath 参数确保了 Tesseract 引擎去读取本地指定目录下的 traineddata 文件。
const languageDataDir = path.join(appDataPath, "ocr-language-data");
const workerCacheDir = path.join(appDataPath, "ocr-worker-cache");
worker = await createWorker('eng+chi_sim', undefined, {
langPath: languageDataDir,
gzip: true,
cachePath: workerCacheDir,
logger: (message) => {
// 记录加载进度与状态
}
});
Tesseract.js 直接处理原始图像时,准确率容易受限于图像质量、对比度和尺寸。在项目实践中,输入给引擎的图像会先使用 sharp 库进行预处理。
图像会被转换为灰度图、调整尺寸、执行标准化和二值化操作。这些操作去除了背景噪音,使文本特征更加明显,进而辅助底层引擎提高识别的准确度。
const sharp = require("sharp");
async function preprocessImage(imagePath, maxWidth, threshold) {
const metadata = await sharp(imagePath).metadata();
let pipeline = sharp(imagePath).rotate();
if (metadata.width && metadata.width > maxWidth) {
pipeline = pipeline.resize({ width: maxWidth, withoutEnlargement: true });
}
pipeline = pipeline.grayscale()
.normalize()
.threshold(threshold, { grayscale: false })
.sharpen();
const output = await pipeline.png().toBuffer({ resolveWithObject: true });
return { buffer: output.data, width: output.info.width, height: output.info.height };
}
通过将处理后的 buffer 直接传递给 Worker 进行识别,避免了中间文件落盘的开销。
Tesseract.js 输出的文本通常包含不需要的换行或多余的空格,特别是在处理中文字符时。本项目在获取到原始识别文本后,会通过正则表达式进行统一的后处理。
主要的清理工作包括将汉字之间的不可见空格去除,以及处理中英文字符和标点符号前后的多余占位符。
function normalizeRecognizedText(text) {
const hanChar = "\\p{Script=Han}";
const spaceClass = "[\\s\\u00A0\\u1680\\u2000-\\u200B\\u202F\\u205F\\u3000]+";
const punctuation = "(?:[,。!?;:、】【《》“”‘’—…]|[,.!?;:]|[()\\[\\]{}<>])";
const removeSpacesBetweenHan = new RegExp(`(?<=${hanChar})${spaceClass}(?=${hanChar})`, "gu");
const removeSpacesBeforePunctuation = new RegExp(`(?<=${hanChar})${spaceClass}(?=${punctuation})`, "gu");
const removeSpacesAfterPunctuation = new RegExp(`(?<=${punctuation})${spaceClass}(?=${hanChar})`, "gu");
return text
.replaceAll("\r\n", "\n")
.split("\n")
.map((line) => line.trim()
.replace(removeSpacesBetweenHan, "")
.replace(removeSpacesBeforePunctuation, "")
.replace(removeSpacesAfterPunctuation, "")
)
.filter(Boolean)
.join("\n");
}
Tesseract 的推理过程是 CPU 密集型的。为了防止阻塞 Electron 的主进程或渲染进程导致应用卡顿,我们将其剥离到了独立的 Node.js 子进程(Worker Process)中运行。
同时,我们通过队列机制确保同一个 Worker 实例串行处理任务,所有的预处理流程和 Worker 调用,都被包裹在这个队列中,避免并发执行导致内存溢出。
class OcrEngine {
constructor() {
this.queue = Promise.resolve();
this.worker = null;
}
enqueue(task) {
const run = this.queue.then(task, task);
this.queue = run.then(() => undefined, () => undefined);
return run;
}
async recognize(imagePath) {
return this.enqueue(async () => {
// 懒加载并复用 worker
if (!this.worker) {
this.worker = await createWorker('eng+chi_sim');
}
const processedBuffer = await preprocessImage(imagePath);
const result = await this.worker.recognize(processedBuffer);
return result.data.text;
});
}
}