React Native Expo收据OCR问题:TypeScript错误与识别优化
解决方案:收据扫描OCR应用问题处理
一、修复expo-camera的TypeScript类型错误
问题根源:Camera是从expo-camera导出的组件值,直接用作TypeScript类型会触发"值被用作类型"的错误。
解决步骤:
- 确保正确导入组件与类型:
import { Camera, CameraType } from "expo-camera";
- 将ref的类型声明为
Camera | null(组件ref初始值为null):
const cameraRef = useRef<Camera | null>(null);
- 若仍报错,检查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
相关产品推荐
相关产品推荐

