You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native Expo收据OCR问题:TypeScript错误与识别优化

解决方案:收据扫描OCR应用问题处理

一、修复expo-camera的TypeScript类型错误

问题根源:Camera是从expo-camera导出的组件值,直接用作TypeScript类型会触发"值被用作类型"的错误。

解决步骤:

  1. 确保正确导入组件与类型:
import { Camera, CameraType } from "expo-camera";
  1. 将ref的类型声明为Camera | null(组件ref初始值为null):
const cameraRef = useRef<Camera | null>(null);
  1. 若仍报错,检查tsconfig.json是否启用以下配置(Expo项目默认已配置):
{
  "compilerOptions": {
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true
  }
}

二、提升OCR识别精度与总金额提取

1. 优化Tesseract.js识别效果

移动端本地OCR受图像质量影响极大,可通过以下方式提升精度:

  • 图像预处理:使用expo-image-manipulator增强图像对比度、统一尺寸,减少识别干扰:
import * as ImageManipulator from "expo-image-manipulator";

const processImage = async (uri: string) => {
  const processedImage = await ImageManipulator.manipulateAsync(
    uri,
    [
      { resize: { width: 1200 } }, // 统一缩放至合适尺寸
      { adjust: { contrast: 1.5, brightness: 0.2 } }, // 增强对比度
      { convert: { format: "png" } } // 转换为无损格式
    ],
    { compress: 0.8, format: ImageManipulator.SaveFormat.PNG }
  );
  return processedImage.uri;
};

在拍照后调用预处理:

const takePicture = async () => {
  if (cameraRef.current) {
    const photo = await cameraRef.current.takePictureAsync({ quality: 0.9 });
    const processedUri = await processImage(photo.uri);
    setImageUri(processedUri);
    extractText(processedUri);
  }
};
  • 配置Tesseract识别参数:限制识别字符范围、指定页面分割模式,针对性优化收据识别:
const extractText = async (uri: Tesseract.ImageLike) => {
  try {
    const result = await Tesseract.recognize(uri, "eng", {
      logger: (m) => console.log(m),
      tessedit_char_whitelist: "0123456789.$,¥¥abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ: ", // 保留收据常见字符
      tessedit_pageseg_mode: 6, // 假设文本为单一均匀块,适配收据布局
    });
    setExtractedText(result.data.text);
    extractTotalAmount(result.data.text);
  } catch (error) {
    console.error("OCR Error:", error);
  }
};

2. 可靠提取总金额

通过正则表达式匹配收据中常见的总金额格式,优先取最后一个匹配项(通常总金额在收据末尾):

const extractTotalAmount = (text: string) => {
  // 匹配多种总金额关键词与格式
  const totalRegex = /(?:total|总计|grand total|amount due)\s*[::]?\s*([$¥¥]?\d+(?:\.\d{2})?)/gi;
  const matches = text.match(totalRegex);
  if (matches) {
    const lastMatch = matches[matches.length - 1];
    const amount = lastMatch.replace(/[^0-9.]/g, ""); // 提取纯数字金额
    setTotalAmount(amount);
  } else {
    Alert.alert("提示", "未识别到总金额");
  }
};

三、React Native Expo中更优的OCR替代方案

Tesseract.js本地运行速度慢、精度有限,推荐以下移动端适配更好的方案:

  • expo-document-scanner:Expo官方文档扫描组件,内置专业图像增强功能,可配合云端OCR服务(如Google Cloud Vision)使用,扫描体验适配移动端。
  • Google ML Kit Text Recognition:通过第三方库集成,本地运行速度快、精度高,专门针对移动端优化,对收据、名片等场景识别效果优于Tesseract.js(需使用Expo Dev Client或eject后集成)。
  • 云端OCR服务:如Google Cloud Vision API、AWS Textract,服务器端处理精度极高,支持结构化字段提取(直接识别金额、日期),适合精度要求高的场景,但需要网络连接且有使用成本。

修改后的完整代码

import React, { useState, useEffect, useRef } from "react";
import { View, Text, TouchableOpacity, Image, StyleSheet, Alert } from "react-native";
import { Camera, CameraType } from "expo-camera";
import Tesseract from "tesseract.js";
import * as ImageManipulator from "expo-image-manipulator";

export default function ReceiptScanner() {
  const [hasPermission, setHasPermission] = useState<boolean | null>(null);
  const [imageUri, setImageUri] = useState<string | null>(null);
  const [extractedText, setExtractedText] = useState("");
  const [totalAmount, setTotalAmount] = useState<string | null>(null);
  const cameraRef = useRef<Camera | null>(null);

  useEffect(() => {
    (async () => {
      const { status } = await Camera.requestCameraPermissionsAsync();
      setHasPermission(status === "granted");
    })();
  }, []);

  const processImage = async (uri: string) => {
    const processedImage = await ImageManipulator.manipulateAsync(
      uri,
      [
        { resize: { width: 1200 } },
        { adjust: { contrast: 1.5, brightness: 0.2 } },
        { convert: { format: "png" } }
      ],
      { compress: 0.8, format: ImageManipulator.SaveFormat.PNG }
    );
    return processedImage.uri;
  };

  const extractTotalAmount = (text: string) => {
    const totalRegex = /(?:total|总计|grand total|amount due)\s*[::]?\s*([$¥¥]?\d+(?:\.\d{2})?)/gi;
    const matches = text.match(totalRegex);
    if (matches) {
      const lastMatch = matches[matches.length - 1];
      const amount = lastMatch.replace(/[^0-9.]/g, "");
      setTotalAmount(amount);
    } else {
      setTotalAmount(null);
      Alert.alert("提示", "未识别到总金额");
    }
  };

  const extractText = async (uri: Tesseract.ImageLike) => {
    try {
      const result = await Tesseract.recognize(uri, "eng", {
        logger: (m) => console.log(m),
        tessedit_char_whitelist: "0123456789.$,¥¥abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ: ",
        tessedit_pageseg_mode: 6,
      });
      setExtractedText(result.data.text);
      extractTotalAmount(result.data.text);
    } catch (error) {
      console.error("OCR Error:", error);
      Alert.alert("错误", "OCR识别失败");
    }
  };

  const takePicture = async () => {
    if (cameraRef.current) {
      const photo = await cameraRef.current.takePictureAsync({ quality: 0.9 });
      const processedUri = await processImage(photo.uri);
      setImageUri(processedUri);
      extractText(processedUri);
    }
  };

  if (hasPermission === null) return <View />;
  if (hasPermission === false) return <Text>No access to camera</Text>;

  return (
    <View style={styles.container}>
      {!imageUri ? (
        <Camera style={styles.camera} ref={cameraRef} />
      ) : (
        <Image source={{ uri: imageUri }} style={styles.imagePreview} />
      )}

      <TouchableOpacity onPress={takePicture} style={styles.button}>
        <Text style={styles.text}>Scan Receipt</Text>
      </TouchableOpacity>

      {totalAmount && (
        <View style={styles.amountContainer}>
          <Text style={styles.amountTitle}>总金额:</Text>
          <Text style={styles.amountText}>${totalAmount}</Text>
        </View>
      )}

      {extractedText ? (
        <View style={styles.textContainer}>
          <Text style={styles.resultTitle}>提取的文本:</Text>
          <Text style={styles.resultText}>{extractedText}</Text>
        </View>
      ) : null}
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, alignItems: "center", justifyContent: "center" },
  camera: { width: "100%", height: "70%" },
  imagePreview: { width: "100%", height: "70%", resizeMode: "contain" },
  button: {
    backgroundColor: "blue",
    padding: 10,
    borderRadius: 5,
    marginTop: 10,
  },
  text: { color: "white", fontSize: 18 },
  textContainer: { marginTop: 20, paddingHorizontal: 10, width: "100%" },
  resultTitle: { fontWeight: "bold", fontSize: 18 },
  resultText: { fontSize: 16, marginTop: 5, lineHeight: 22 },
  amountContainer: {
    marginTop: 15,
    padding: 12,
    backgroundColor: "#f0f0f0",
    borderRadius: 8,
    width: "90%",
    alignItems: "center",
  },
  amountTitle: { fontSize: 18, fontWeight: "bold" },
  amountText: { fontSize: 24, color: "green", marginTop: 5 },
});

内容的提问来源于stack exchange,提问作者Ayana Sagara

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 18:47:05