基于浏览器的收据扫描器LiteRT.js

谷歌最近推出了LiteRT.js,一款用于运行设备内AI推理的浏览器运行时,使用WebAssembly和WebGPU。

LiteRT.js将谷歌的LiteRT运行时(前身为TensorFlow Lite)带到了网络上。它无需采用JavaScript专用的模型格式,而是可以在浏览器中直接运行标准模型,同时利用现代浏览器硬件加速。.tflite

在本教程中,我们将了解LiteRT.js的工作原理,并构建端到端光学字符识别(OCR)收据扫描器。该应用程序将预处理收据照片,本地检测和识别文本,重建文档布局,并将提取的文本通过LiteRT-LM传递给设备上的Gemma模型以结构化结果。

整个流程在浏览器本地运行,因此收据数据无需发送到外部推理 API。

前提条件

你需要:

  • Node.js 20岁或更晚

  • 一个React和TypeScript项目,最好用Vite创建

  • 如果你想用GPU加速,可以推荐支持WebGPU的浏览器

在我们构建扫描仪之前,先看看在浏览器中运行机器学习模型时LiteRT.js有哪些变化。

理解LiteRT.js

TensorFlow Lite 最初是为移动和嵌入式系统设计的修复:iPhone 键盘上缺少麦克风图标 - 致知笔记 - 专注教程知识分享,而 TensorFlow.js 则专为网络设计。

然而,随着浏览器获得 WebAssembly SIMD 和 WebGPU 等功能,原生推理与基于浏览器的推理差距逐渐缩小。随后,谷歌将TensorFlow Lite重新命名为LiteRT,作为其更广泛的AI边缘工具的一部分。

LiteRT 既是一个模型运行时,也是更广泛转换流程的一部分。源自 TensorFlow、PyTorch 和 JAX 等框架的模型最终可以部署到该格式,而 LiteRT.js 则将该运行时带到浏览器中。.tflite

对于网页应用,重要的区别在于LiteRT.js可以使用现代浏览器计算API执行模型,而不必要求模型针对特定的JavaScript执行环境。.tflite

LiteRT.js后端

LiteRT.js可以针对多种执行路径:

后端 职责 最适合
WebAssembly + XNNPACK CPU 执行与备份 广泛兼容性与CPU推理
WebGPU GPU加速计算 并行工作负载,如神经网络推理
WebNN 新兴硬件抽象 直接访问可用的机器学习加速器和NPU

WebAssembly 和 XNNPACK

WebAssembly 提供浏览器端的执行环境,而 XNNPACK 则提供优化的神经网络操作符。

借助 SIMD 和多线程等浏览器功能,修复:天气应用在 iPhone 上无法正常工作 - 致知笔记 - 专注教程知识分享这为LiteRT.js提供了比直接在 JavaScript 中实现相同数值操作更快的 CPU 路径。

WebGPU

WebGPU 为浏览器应用程序提供了通用 GPU 计算的访问权限。LiteRT.js可以利用该能力在用户GPU上执行高度并行的操作,如矩阵乘法。

这避免了将主要为图形设计的WebGL作为通用计算API的许多限制。

WebNN

WebNN是一种新兴的神经网络加速浏览器API。在支持的情况下,旨在提供对设备可用机器学习硬件的访问,包括GPU和NPU。

项目的建立

我们先初始化React应用并安装依赖。

申请主要分为两个阶段:

  1. 一个检测和识别文本的OCR流水线

  2. 一个将OCR输出转换为结构化收据数据的LLM流水线

如果你还没创建项目,可以用Vite搭建一个React和TypeScript应用:

npm create vite@latest document-scanner -- --template react-ts

然后安装LiteRT.js:

npm install @litertjs/core

我们将用来传递TensorFlow.js和LiteRT.js之间的张量:@litertjs/tfjs-interop

npm install @litertjs/tfjs-interop

安装TensorFlow.js:

npm install @tensorflow/tfjs

然后添加它的WebGPU后端:

npm install @tensorflow/tfjs-backend-webgpu

最后,安装 LiteRT-LM 用于设备内语言模型:

npm install --save @litert-lm/core

最终的堆栈如下:

包装 目的
@litertjs/core 加载和执行模型.tflite
@litertjs/tfjs-interop TensorFlow.js与LiteRT.js共享张量
@tensorflow/tfjs 张量操作与支持图像操作
@tensorflow/tfjs-backend-webgpu WebGPU 后端支持TensorFlow.js
@litert-lm/core 运行设备内语言模型

添加OCR和LLM模型

OCR流水线需要三个文件:

  • 文本检测模型

  • 文本识别模型

  • 一个将识别输出类映射回字符的词典

你可以在GitHub仓库中找到OCR模型、词典和完整项目。

Gemma型号可在Hugging Face购买。

将所需的模型文件放入项目目录中。public/models/

本教程中使用的代码分布在 ,组织为三个主要目录:src/

src/
├── litert/
│   ├── runtime.ts
│   └── models.ts
├── ocr/
│   ├── preprocess.ts
│   ├── detect.ts
│   ├── ctc.ts
│   └── layout.ts
└── llm/
    ├── engine.ts
    └── structureWithLlm.ts

这种分离使模型初始化、OCR处理和LLM推断保持独立。

构建本地LiteRT.js运行时

我们先从机器学习运行时本身说起。

在推理运行前,需要初始化两个部件:

  • 浏览器计算后端

  • 编译后的OCR模型

初始化GPU运行时

初始化机器学习后端成本相对较高。React组件可以频繁挂载、卸载和重新渲染,因此将运行时初始化直接绑定到组件生命周期可能会导致重复的GPU上下文和不必要的内存压力。

相反,我们将在模块层级缓存初始化,如何在 iPhone 上获取天气提醒和通知 - 致知笔记 - 专注教程知识分享并共享 。Promise

创作:runtime.ts

import {
  loadLiteRt,
  getWebGpuDevice,
  isWebGPUSupported
} from '@litertjs/core';
import * as tf from '@tensorflow/tfjs';
import { WebGPUBackend } from '@tensorflow/tfjs-backend-webgpu';
import { WASM_PATH } from '../ocr/config';
​
export interface RuntimeInfo {
  webgpu: boolean;
  tfjsBackend: string;
}
​
let runtimePromise: Promise<RuntimeInfo> | null = null;
​
export function initRuntime(): Promise<RuntimeInfo> {
  if (!runtimePromise) {
    runtimePromise = doInit().catch((err) => {
      runtimePromise = null;
      throw err;
    });
  }
​
  return runtimePromise;
}
​
async function doInit(): Promise<RuntimeInfo> {
  if (isWebGPUSupported()) {
    await tf.setBackend('webgpu');
    await tf.ready();
    await loadLiteRt(WASM_PATH);
​
    const device = getWebGpuDevice();
​
    if (device) {
      tf.removeBackend('webgpu');
      tf.registerBackend(
        'webgpu',
        () => new WebGPUBackend(device, device.adapterInfo)
      );
​
      await tf.setBackend('webgpu');
      await tf.ready();
​
      return {
        webgpu: true,
        tfjsBackend: tf.getBackend()
      };
    }
  }
​
  await loadLiteRt(WASM_PATH);
  await tf.setBackend('cpu');
  await tf.ready();
​
  return {
    webgpu: false,
    tfjsBackend: tf.getBackend()
  };
}

runtimePromise确保多个呼叫者共享相同的初始化工作。如果初始化失败,承诺会被重置,以便后续请求可以再次尝试。

当 WebGPU 可用时,应用程序会初始化 TensorFlow.js WebGPU 后端,并与 LiteRT.js 共享该 GPU 设备。否则,它会退回到CPU执行。

OCR模型的编译

运行时准备好后,我们可以编译模型图。.tflite

编译为特定执行后端的模型做准备。这里,我们先尝试首选的加速后端,如果编译失败再回退。

创作:models.ts

import { loadAndCompile } from '@litertjs/core';
import type {
  CompiledModel,
  TensorDetails
} from '@litertjs/core';

async function compileWithFallback(
  url: string,
  order: readonly Backend[]
): Promise<LoadedModel> {
  let lastErr: unknown;

  for (const accelerator of order) {
    try {
      const model = await loadAndCompile(url, { accelerator });

      const toSpec = (d: TensorDetails) => ({
        name: d.name,
        dtype: d.dtype,
        shape: Array.from(d.shape)
      });

      return {
        model,
        backend: accelerator,
        inputs: model.getInputDetails().map(toSpec),
        outputs: model.getOutputDetails().map(toSpec)
      };
    } catch (err) {
      lastErr = err;
    }
  }

  throw new Error(`Failed to compile ${url}: ${String(lastErr)}`);
}

完整实现并行加载检测器、识别器和字典。models.ts``Promise.all

预处理收据图像

在通过OCR模型发送图像之前,我们会预处理以提高识别可靠性。

管道:

  1. 放大图像

  2. 将图像转换为灰度

  3. 拉伸对比度,使文字更突出

相关部分看起来是这样的:preprocess.ts

export function preprocess(
  source: CanvasImageSource,
  srcW: number,
  srcH: number
): Preprocessed {
  const scale = downscaleFactor(srcW, srcH);
  const w = Math.round(srcW * scale);
  const h = Math.round(srcH * scale);

  const img = toImageData(source, w, h);
  const luma = grayscale(img);
  contrastStretch(img, luma);

  return {
    image: img,
    scale
  };
}

downscaleFactor()将最长边限制在1600像素,防止不必要的大图像增加推理成本。

灰度转换将图像简化为亮度信息,而对比度拉伸则增加文本与背景之间的分离度。

本地运行OCR推理

OCR流程分为三个阶段:

  1. 检测文本区域

  2. 识别每个区域的文本

  3. 从检测到的坐标重建线和列

文本检测

检测模型会找到文本边界框。

当WebGPU可用时,我们希望避免在GPU和CPU内存之间反复复制张量。 提供 ,使TensorFlow.js张量直接进入LiteRT.js执行路径。@litertjs/tfjs-interop``runWithTfjsTensors

相关代码如下:detect.ts

import * as tf from '@tensorflow/tfjs';
import { runWithTfjsTensors } from '@litertjs/tfjs-interop';

export async function detect(
  det: LoadedModel,
  image: ImageData
): Promise<{ boxes: Box[] }> {
  const inLayout = {
    h: det.inputs[0].shape[2],
    w: det.inputs[0].shape[3],
    layout: 'nchw' as const
  };

  const input = buildInput(
    image,
    inLayout.w,
    inLayout.h,
    inLayout.layout
  );

  const outputs = await runWithTfjsTensors(det.model, [input]);
  input.dispose();

  const [region, affinity] = await Promise.all([
    outputs[0].data(),
    outputs[1].data()
  ]);

  tf.dispose(outputs);

  return {
    boxes: decodeCraftHeatmaps(
      region,
      affinity,
      inLayout.w,
      inLayout.h
    )
  };
}

buildInput()将图像值归一化到模型预期的范围。

推断后,模型的热图通过连通分量分组法解码为包围框。

识别文本

一旦确定了文本区域,每个区域都会被裁剪并传递给识别模型。

识别器在每个时间步返回字符类的概率分布。然后我们用连接主义时间分类(CTC)解码该输出。

解码器看起来像这样:ctc.ts

export function ctcGreedyDecode(
  logits: Float32Array,
  T: number,
  numClasses: number,
  chars: string[]
) {
  const path = new Int32Array(T);
  const probs = new Float32Array(T);

  for (let t = 0; t < T; t++) {
    path[t] = getArgmax(
      logits,
      t * numClasses,
      numClasses
    );

    probs[t] = getSoftmaxProbability(
      logits,
      t * numClasses,
      numClasses
    );
  }

  let out = '';
  let prev = -1;
  const blank = 0;

  for (let t = 0; t < T; t++) {
    const cls = path[t];

    if (cls !== prev && cls !== blank) {
      out += chars[cls] ?? '';
    }

    prev = cls;
  }

  return {
    text: out,
    confidence: calculateAvgConfidence(probs)
  };
}

解码器会在每个时间步中选取最可能的字符,折叠重复的类,并移除CTC空白标记。

重建收据布局

原始OCR输出不足以获得收据。我们还需要保持物品名称及其价格出现在同一行的关系。

应用程序利用每个检测到的字的坐标重建该布局。

载于:layout.ts

export function reconstructLines(
  words: Word[],
  rowTolerance = 0.6
): Line[] {
  const byY = [...words].sort(
    (a, b) =>
      (a.box.y + a.box.h / 2) -
      (b.box.y + b.box.h / 2)
  );

  const rows: Row[] = [];

  for (const word of byY) {
    const matchedRow = findMatchingRow(
      rows,
      word,
      rowTolerance
    );

    if (matchedRow) {
      matchedRow.words.push(word);
    } else {
      rows.push({
        words: [word],
        centerSum: word.box.y + word.box.h / 2
      });
    }
  }

  return rows
    .map(formatAndInsertSpaces)
    .sort((a, b) => a.yCenter - b.yCenter);
}

该算法首先将垂直位置在容差范围内重叠的单词分组。然后它会水平排序单词,并根据彼此之间的物理距离插入空格。

这使得LLM比单纯的识别字符串列表更能用地表示收据。

使用 LiteRT-LM 结构化收据数据

OCR给我们提供文本。下一步是将这些文本转化为结构化应用数据。

对于收据来说,这可能意味着:

  • 商人名称

  • 各项项

  • 价格

  • 税务

  • 总计

我们将使用设备内的 Gemma 模型,通过 LiteRT-LM 将重建后的 OCR 输出转换为 JSON。如果你对构建多回合AI代理的其他方法感兴趣,Genkit的代理API值得作为补充选项探索。

对LLM运行时间进行懒散加载

LLM运行时和模型资源相对较大,因此将它们作为初始应用包的一部分加载,即使是从未使用过该功能的用户,也会增加启动成本。

相反,只有当用户启用带有LLM的结构选项时,我们才会动态导入LiteRT-LM。

相关部分:engine.ts

import type { Engine } from '@litert-lm/core';
import {
  LLM_MODEL_PATH,
  LLM_MAX_TOKENS,
  LLM_WASM_PATH
} from '../ocr/config';

let status: LlmStatus = 'unavailable';
let enginePromise: Promise<Engine | null> | null = null;

export function initLlmEngine(): Promise<Engine | null> {
  if (!enginePromise) {
    enginePromise = doInit().catch(() => {
      enginePromise = null;
      return null;
    });
  }

  return enginePromise;
}

async function doInit(): Promise<Engine | null> {
  const mod = await import('@litert-lm/core');

  await mod.getOrLoadGlobalLiteRtLm(LLM_WASM_PATH);

  return await mod.Engine.create({
    model: LLM_MODEL_PATH,
    mainExecutorSettings: {
      maxNumTokens: LLM_MAX_TOKENS
    }
  });
}

模块级承诺的作用与主LiteRT.js运行时相同:一次只运行一次初始化。

这种动态还让捆绑者有机会将LLM运行时拆分为独立块,而不是包含在应用的初始JavaScript捆包中。这种懒惰的评估是管理昂贵资源的React应用中常见的模式。import()

运行LLM推理

接下来,我们提供了一个系统提示,定义预期的输出格式和目标 JSON 模式。

structureWithLlm.ts处理请求:

import { getLlmEngine } from './engine';
import {
  SYSTEM_PROMPT,
  buildUserPrompt
} from './prompt';

export async function structureWithLlm(
  lines: Line[]
): Promise<ReceiptData> {
  const engine = await getLlmEngine();

  if (!engine) {
    return structureFallback(lines);
  }

  try {
    const convo = await engine.createConversation({
      preface: {
        messages: [
          {
            role: 'system',
            content: SYSTEM_PROMPT
          }
        ]
      }
    });

    const prompt = buildUserPrompt(lines);
    const raw = await convo.sendMessage(prompt);

    await convo.delete();

    const jsonText = extractJson(raw.content);

    return parseAndMapOcrConfidence(
      jsonText,
      lines
    );
  } catch (err) {
    return structureFallback(lines);
  }
}

如果LLM无法初始化或推理失败,应用会退回到基于正则表达式的解析器,而不是整个扫描失败。

浏览器中的结果如下:

注:本次演示中使用的识别模型在作者测试中识别准确率不到50%。你可以用更准确的OCR模型替换它,而不会改变整体LiteRT.js流程。

LiteRT.js 与其他浏览器机器学习运行时的比较

LiteRT.js 是浏览器中运行机器学习工作负载的多种选项之一。

主要区别在于模型格式、执行架构以及每个运行时支持的硬件后端。

特色 LiteRT.js TensorFlow.js ONNX 运行时网页
模型格式 .tflite TensorFlow.js模型格式 .onnx
CPU 执行 Wasm / 优化的原生内核 JavaScript / Wasm 后端 瓦斯姆
GPU执行 WebGPU WebGL / WebGPU,取决于后端 WebGPU
框架互操作性 模型可以通过转换来源于多个机器学习框架 与TensorFlow生态系统最强 广泛的ONNX生态系统
WebNN路径 新生/依赖支持 依赖支持者 依赖支持者

最重要的选择通常是你已经使用的模型格式。

如果你已经有模型,或者针对同一模型流水线的Android、移动端、嵌入式和网页,LiteRT.js尤其有吸引力,因为浏览器可以使用相同的部署格式。.tflite

TensorFlow.js仍然拥有更广泛的JavaScript原生生态系统,当张量操作和模型执行主要都存在于JavaScript中时,它仍然非常有用。在评估这类机器学习项目的包管理器和依赖设置时,值得考虑每个工具如何影响安装时间和磁盘使用情况。

当你的模型流水线已经针对ONNX或你需要兼容多个训练框架的模型时,ONNX运行时Web是非常合适的选择。

结论

LiteRT.js为网页开发者提供了另一种实用的路径,可以直接在用户设备上运行机器学习模型。

在本教程中,我们构建了一个基于浏览器的收据处理流程,利用LiteRT.js进行OCR推断,重建识别文本的空间布局,并可选择性地将结果通过设备内的LiteRT-LM语言模型处理。

该架构还展示了LiteRT.js与现有浏览器工具的定位。TensorFlow.js 在这里仍然在张量处理和互操作性中发挥着有用作用,而 LiteRT.js 则负责模型执行。这使得LiteRT.js不再是全面TensorFlow.js替代品,而是围绕谷歌AI边缘模型生态系统构建的应用的新选择。.tflite

本地运行推理也会改变应用的部署模型。图像和提取后的文本可以保留在用户设备上,应用程序可以继续运行而无需往返推理 API,开发者也可以避免为每个模型请求建立专用后台。对于评估AI在其更广泛产品战略中定位的团队来说,随着这些设备端能力的成熟,了解AI知识产品经理的需求变得越来越重要。

posted @ 2026-09-13 23:05  panxianren  阅读(3)  评论(0)    收藏  举报