黑白梦黑白梦

toggle navtoggle nav
  • 文章
  • 专栏
  • 文章
  • 专栏

Tesseract.js 构建本地图像文字识别模块

发布于 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 来实现引擎的复用,从而大幅提升后续识别任务的处理速度。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;
    });
  }
}
目录
基础用法使用 Worker多语言离线语言包图像预处理提升识别率文本后处理与清理任务队列与并发控制

©2015-2026 黑白梦 粤ICP备15018165号

联系: heibaimeng@foxmail.com