当前位置: 首页 > article >正文

【前端】如何依靠纯前端实现拍照获取/选择文件等文字识别OCR技术

本文仅介绍第三方依赖包Tesseract.js,Tesseract.js 是一个基于网页的 OCR(光学字符识别)引擎,可以识别图像中的文本并将其转换为可供计算机处理的文本数据。

支持多框架编译,如Vue、React等,这里展示步骤为React开发。

下面直接进入主题:附Tesseract.js官方(https://github.com/naptha/tesseract.js)

  1. 下载安装依赖包
npm install tesseract.js
# 或者
yarn add tesseract.js
  1. 在对应的页面或者App.js中引入,如OCRTest.js页面
import Tesseract from 'tesseract.js';
  1. 初始化一个Input,用于选择图片使用,并且加入一个日志输出框,以及对应的预览图,识别文本,识别耗时,转换后的文本展示等HTML内容。界面Demo如下:

在这里插入图片描述

  1. 在对应的Input触发选中方法中,handleFileChange初始化预览图,在点击button开始识别,更改状态并执行tesseract初始化,识别文字,根据正则匹配截取自己需要的文本,并且开始计时。

3-4步骤完整代码Demo如下:注意这里用了Hook写法,因为需求是获取一串数字,所以英文包可能存在把数字8识别成英文B,要做兼容。

其中LSTM 引擎和假设单行文本以及白名单之类的可配置也可不配,没有配置会自动选择引擎。具体要根据识别结果去慢慢调整。

而英文包或中文包也是可选项,一般针对性强一点只获取英文数字则选择英文包,因为中文识别机制总会会相对难一点,准确率更低些。

import React, { useState, useEffect } from 'react';
import Tesseract from 'tesseract.js';
import { Header } from 'components'

function OcrComponent() {
  const prefixes = '783|784|731'; // 暂定的数字开头前缀,用于匹配识别文本
  const [file, setFile] = useState(null);
  const [inputText, setInputText] = useState('');
  const [inputOCRText, setInputOCRText] = useState('');
  const [inputLoading, setInputLoading] = useState(false);
  const [inputTime, setInputTime] = useState(null);
  const [previewInputUrl, setPreviewInputUrl] = useState(null);
  const [logMessages, setLogMessages] = useState([]);

  // 兼容措施,如果有7开头并且后面连着12个字符的,可能会把数字“8”被识别为英文“B”,
  const replaceBWith8 = (text) => {
    // 定义正则
    const pattern = /7(?=(?:\d|B){11}\d)([\dB]{11})/g;

    return text.replace(pattern, (match, p1) => {
      // 替换'B'为'8'
      return '7' + p1.replace(/B/g, '8');
    });
  }

  // OCR处理函数
  const handleOcr = async (source, setTextFn, setLoadingFn, setOcrTextFn, logger) => {
    setLoadingFn(true);
    setTextFn('');

    try {
      // 创建一个新的Promise来处理Tesseract识别过程。
      const result = await Tesseract.recognize(
        source,
        // 'chi_sim', // 中文简体
        'eng', // 可以根据需要指定语言包
        // 'chi_sim+eng', // 使用中文和英文语言包
        {
          logger: m => {
            // 更新日志消息,限制日志条目数量以避免内存泄漏
            setLogMessages(prevMessages => [m, ...prevMessages.slice(0, 9)]);
          },
          tessedit_ocr_engine_mode: 1, // 使用 LSTM 引擎 (OEM_LSTM_ONLY)
          tessedit_pageseg_mode: 6,   // 假设单行文本 (PSM_SINGLE_LINE)
          // tessedit_char_whitelist: '1234567890', // 识别的字符白名单
          // tessedit_zero_rejection: true // 允许零置信度的识别结果
        }
      );

      console.log('识别出来的文本:', result.data.text);
      setLogMessages((prevMessages) => [`识别结束出来的文本:${result.data.text}`, ...prevMessages]);
      // 替换'B'为'8'
      let resultText = replaceBWith8(result.data.text);
      console.log('转换后的文本:', resultText);

      setOcrTextFn(resultText);

      // 使用正则查找数字
      const regex = new RegExp(`(?:\D|^)(${prefixes})\d{10}`, 'g');

      // 创建一个空数组用于存储匹配结果
      let matches = [];
      let match;

      while ((match = regex.exec(resultText)) !== null) {
        // 确保我只获取到13位的数字序列
        const extractedNumber = match[0].slice(-13); // 从匹配结果中提取最后13个字符
        matches.push(extractedNumber);
      }

      console.log('匹配到的所有结果:', matches);

      // 将匹配的结果映射为仅包含13位数字的字符串数组,并去重
      const passenger = [...new Set(matches)];
      console.log('获取到的数字:', passenger);

      // 设置数字
      setTextFn(passenger.length > 0 ? passenger.join(', ') : '');

    } catch (error) {
      console.error('Error during OCR:', error);
      setLogMessages((prevMessages) => [`识别失败: ${error.message}`, ...prevMessages]);
    } finally {
      setLoadingFn(false);
    }
  };

  useEffect(() => {
    return () => {
      if (previewInputUrl) {
        URL.revokeObjectURL(previewInputUrl);
      }
    };
  }, [previewInputUrl]);


  // input框识别触发转换图片
  const handleFileChange = (event) => {
    const selectedFile = event.target.files[0];
    if (selectedFile) {
      setFile(selectedFile);
      // 创建一个对象URL以预览图片
      const objectUrl = URL.createObjectURL(selectedFile);
      setPreviewInputUrl(objectUrl);

      // 组件卸载时释放对象URL
      return () => URL.revokeObjectURL(objectUrl);
    } else {
      setPreviewInputUrl(null);
    }
  };

  // input框识别触发OCR识别
  const handleInputOcr = async () => {
    if (!file) return;
    console.log('上传的文件', file);

    await handleOcr(
      file,
      setInputText,
      setInputLoading,
      setInputOCRText,
      (m) => console.log(m) // 自定义日志记录器
    );
  };

  // 用于测量执行时间的通用函数
  const getTime = async (asyncFunction, ...args) => {
    const startTime = performance.now();

    // 执行传入的异步函数并传递参数
    await asyncFunction(...args);

    const endTime = performance.now();
    const executionTime = (endTime - startTime).toFixed(2); // 保留两位小数
    console.log(`执行时间: ${executionTime} 毫秒`);
    return executionTime;
  };

  // input点击OCR识别
  const clickInputIdentify = async () => {
    setInputOCRText('')
    setInputText('')
    setInputTime('')
    // 调用通用的getTime函数,传入handleInputOcr以及可能需要的参数
    const executionTime = await getTime(handleInputOcr);
    setInputTime(executionTime);
  };

  const resultStyle = {
    marginTop: '20px',
    backgroundColor: '#fff',
    padding: '10px',
    border: '1px solid #ddd',
    borderRadius: '4px',
    whiteSpace: 'pre-wrap',
    wordWrap: 'break-word',
    fontFamily: 'monospace'
  };

  return (
    <div >
      <Header title={'OCR测试'} />
      <h2>测试-OCR-Tesseract.js</h2>
      <div style={{ marginTop: 20 }}>
        <h3>本地图片上传测试区域</h3>
        <div style={{ marginTop: 20 }}>
          选择图库文件区域:
          <input type="file" onChange={handleFileChange} accept="image/*" />
        </div>
        {previewInputUrl && (
          <div style={{
            textAlign: 'center',
            margin: '20px'
          }} >
            <div>预览图</div>
            <img src={previewInputUrl} style={{
              maxWidth: '100%',
              height: '100px',
              borderRadius: '4px'
            }} />
          </div>
        )}
        <button onClick={clickInputIdentify} disabled={!file || inputLoading} style={{ marginTop: 20 }}>
          {inputLoading ? '识别加载中...下方日志' : '点击开始识别'}
        </button>
        {/* 日志输出容器 */}
        <div style={{ marginTop: 20 }}>
          日志输出:
          <div
            id="log-container"
            style={{
              height: '200px',
              overflowY: 'scroll',
              border: '1px solid #ccc',
              padding: '5px',
              marginTop: '10px',
            }}
          >
            {logMessages.map((message, index) => (
              <p key={index}>{JSON.stringify(message)}</p>
            ))}
          </div>
        </div>

        <div style={resultStyle}>数字:{inputText ? inputText : 'input无识别结果'}</div>
        <div style={resultStyle}>识别出来并转换的全部文本:{inputOCRText ? inputOCRText : '暂无识别'}</div>
        <div style={resultStyle}>识别耗时:{inputTime}毫秒</div>
      </div>
    </div>
  );
}

export default OcrComponent;
  1. 根据以上代码,并在谷歌进行本地测试,识别后可以得到以下内容作为参考,到这里第一步识别文本就是成功的,后续就是根据实际需求去调整规则,以及对图片本身清晰度做调整。

在这里插入图片描述

  1. 进一步要做拍照识别,没有其他要求就直接调用原生摄像头进行拍照获取。同样用Input组件,并且设定capture=“camera”,拍摄下来的图片会因为浏览器差异化,部分会存在图库,部分则只是临时链接存在手机缓存,不会直接在图库中显示。
<div style={{ marginTop: 20 }}>
     点击直接唤起原生摄像头区域:
    <input type="file" onChange={handleFileChange} capture="camera" accept="image/*" />
</div>
  1. 拍照识别对拍摄技巧的图片要求更为苛刻,比如曝光度、大小、像素等。也会出现乱码的情况。以下为苹果16真机结果,正常现象,跟图片关系很大,要多尝试。(免费的东西就不要跟商业的对比精准度了)

在这里插入图片描述

  1. 如果要进一步做扫一扫动态识别,只能在页面自己创建一个摄像区。那么就要用到video标签以及引入第三方webrtc-adapter的支持,解决getUserMedia不存在提示undefined的问题,同时要确保浏览器等运行环境已经获取到允许执行摄像头的权限。并且只能在HTTPS协议下运行。

并且要把拍摄到的图片转为canvas,做base64转换,才能确保不会存在手机图库中。同时要注意控制视频流,做摄像头开关拍照控制等。

以下为代码参考:

import React, { useState, useRef, useEffect } from 'react';
import Tesseract from 'tesseract.js';
import { Header } from 'components'
import 'webrtc-adapter';

function OcrComponent() {
  const prefixes = '783|784|731'; // 暂定的数字开头前缀,用于匹配识别文本
  const [isCameraOn, setIsCameraOn] = useState(false);
  const videoRef = useRef(null);
  const canvasRef = useRef(null);
  const streamRef = useRef(null);
  const [photoText, setPhotoText] = useState('');
  const [loading, setLoading] = useState(false);
  const [previewUrl, setPreviewUrl] = useState(null);
  const [photoOCRText, setPhotoOCRText] = useState('');
  const [photoTime, setPhotoTime] = useState(null);


  // 兼容措施,如果有7开头并且后面连着12个字符的,可能会把数字“8”被识别为英文“B”,
  const replaceBWith8 = (text) => {
    // 定义正则
    const pattern = /7(?=(?:\d|B){11}\d)([\dB]{11})/g;

    return text.replace(pattern, (match, p1) => {
      // 替换'B'为'8'
      return '7' + p1.replace(/B/g, '8');
    });
  }

  // OCR处理函数
  const handleOcr = async (source, setTextFn, setLoadingFn, setOcrTextFn, logger) => {
    setLoadingFn(true);
    setTextFn('');

    try {
      // 创建一个新的Promise来处理Tesseract识别过程。
      const result = await Tesseract.recognize(
        source,
        // 'chi_sim', // 中文简体
        'eng', // 可以根据需要指定语言包
        // 'chi_sim+eng', // 使用中文和英文语言包
        {
          logger: m => {
            // 更新日志消息,限制日志条目数量以避免内存泄漏
            console.log(m)
          },
          tessedit_ocr_engine_mode: 1, // 使用 LSTM 引擎 (OEM_LSTM_ONLY)
          tessedit_pageseg_mode: 6,   // 假设单行文本 (PSM_SINGLE_LINE)
        }
      );

      console.log('识别出来的文本:', result.data.text);
      // 替换'B'为'8'
      let resultText = replaceBWith8(result.data.text);
      console.log('转换后的文本:', resultText);

      setOcrTextFn(resultText);

      // 使用正则查找数字
      // 匹配13位数的数字,其中前三位是变量prefixes中含有的开头,后续10位的数字
      const regex = new RegExp(`(?:\D|^)(${prefixes})\d{10}`, 'g');

      // 创建一个空数组用于存储匹配结果
      let matches = [];
      let match;

      while ((match = regex.exec(resultText)) !== null) {
        // 确保我只获取到13位的数字序列
        const extractedNumber = match[0].slice(-13); // 从匹配结果中提取最后13个字符
        matches.push(extractedNumber);
      }

      console.log('匹配到的所有结果:', matches);

      // 将匹配的结果映射为仅包含13位数字的字符串数组,并去重
      // const passenger = resultText.match(regex);
      const passenger = [...new Set(matches)];
      console.log('获取到的数字:', passenger);

      // 设置数字
      setTextFn(passenger.length > 0 ? passenger.join(', ') : '');

    } catch (error) {
      console.error('Error during OCR:', error);
    } finally {
      setLoadingFn(false);
    }
  };

  // 开启摄像头
  const startCamera = async () => {
    try {
      // 指定使用后置摄像头
      const constraints = {
        video: {
          facingMode: "environment" // 后置摄像头
        }
      };

      const newStream = await navigator.mediaDevices.getUserMedia(constraints);
      if (videoRef.current) {
        videoRef.current.srcObject = newStream;
        streamRef.current = newStream; // 保存流引用
      }
    } catch (error) {
      console.error('Error accessing media devices.', error);
      alert(`无法访问摄像头,请检查权限设置${error}`);
    }
  };


  // 关闭摄像头
  const stopCamera = () => {
    if (streamRef.current) {
      streamRef.current.getTracks().forEach(track => track.stop());
      streamRef.current = null;
      if (videoRef.current) {
        videoRef.current.srcObject = null;
      }
    }
  };

  // 根据摄像头状态启动或停止摄像头
  useEffect(() => {
    if (isCameraOn) {
      startCamera();
    } else {
      stopCamera();
    }

    return () => {
      // 组件卸载时停止摄像头
      stopCamera();
    };
  }, [isCameraOn]);


  // 用于测量执行时间的通用函数
  const getTime = async (asyncFunction, ...args) => {
    const startTime = performance.now();

    // 执行传入的异步函数并传递参数
    await asyncFunction(...args);

    const endTime = performance.now();
    const executionTime = (endTime - startTime).toFixed(2); // 保留两位小数
    console.log(`执行时间: ${executionTime} 毫秒`);
    return executionTime;
  };

  // 摄像头点击OCR识别
  const handleTakePhoto = async () => {
    if (!isCameraOn) {
      alert('请先点击开启摄像头')
      return
    }
    setPhotoOCRText('')
    setPhotoTime('')
    setPhotoText('')
    // 暂停视频流
    const tracks = streamRef.current?.getTracks();
    tracks?.forEach(track => track.enabled = false);

    // 获取视频的实际尺寸
    const videoWidth = videoRef.current.videoWidth;
    const videoHeight = videoRef.current.videoHeight;

    // 创建一个临时 canvas 来获取原始比例的截图
    const tempCanvas = document.createElement('canvas');
    const tempContext = tempCanvas.getContext('2d');
    tempCanvas.width = videoWidth;
    tempCanvas.height = videoHeight;
    tempContext.drawImage(videoRef.current, 0, 0, videoWidth, videoHeight);

    // 设置最终输出的尺寸(与 <video> 显示的尺寸一致)
    const outputWidth = videoRef.current.offsetWidth; // 使用CSS宽度
    const outputHeight = videoRef.current.offsetHeight; // 使用CSS高度

    // 在主 canvas 上绘制调整后的图像
    canvasRef.current.width = outputWidth;
    canvasRef.current.height = outputHeight;
    const context = canvasRef.current.getContext('2d');
    context.drawImage(tempCanvas, 0, 0, outputWidth, outputHeight);

    // 将 canvas 转换为 base64 图像数据
    const base64Image = canvasRef.current.toDataURL('image/png');
    setPreviewUrl(base64Image);
    console.log('拍摄的图片', base64Image);
    // 执行OCR识别
    const executionTime = await getTime(handlePhotoOcr, base64Image);
    setPhotoTime(executionTime);

    // 立刻关闭摄像头
    stopCamera();

    // 更新摄像头状态为关闭
    setIsCameraOn(false);
  };

  // 拍照后转换为base64-OCR识别
  const handlePhotoOcr = async (base64Image) => {
    await handleOcr(
      base64Image,
      setPhotoText,
      setLoading,
      setPhotoOCRText,
      (m) => console.log(m) // 自定义日志记录器
    );
  };

  const resultStyle = {
    marginTop: '20px',
    backgroundColor: '#fff',
    padding: '10px',
    border: '1px solid #ddd',
    borderRadius: '4px',
    whiteSpace: 'pre-wrap',
    wordWrap: 'break-word',
    fontFamily: 'monospace'
  };

  return (
    <div >
      <Header title={'OCR测试'} />
      <h2>测试-OCR-Tesseract.js</h2>

      {/* 拍照测试区 */}
      <div style={{ marginTop: 20 }}>
        <h3> 拍照/视频上传base64测试区域</h3>
        <button onClick={() => setIsCameraOn(prevState => !prevState)} style={{ marginTop: 20 }}>
          {isCameraOn ? '关闭摄像头' : '开启摄像头'}
        </button>
        <video ref={videoRef} autoPlay muted playsInline style={{ width: '100%', height: '300px', objectFit: 'cover' }} />
        <button onClick={handleTakePhoto} disabled={!isCameraOn || loading}>
          {loading ? '加载中...' : '点击拍照识别'}
        </button>
        {previewUrl && (
          <div style={{
            textAlign: 'center',
            margin: '20px'
          }}>
            <div>预览图</div>
            <img style={{
              maxWidth: '100%',
              height: '100px',
              borderRadius: '4px'
            }} src={previewUrl} />
          </div>
        )}
        <canvas ref={canvasRef} style={{ display: 'none' }} />
        <div style={resultStyle}>数字:{photoText ? photoText : '拍照无识别结果'}</div>
        <div style={resultStyle}>识别出来并转换的全部文本:{photoOCRText ? photoOCRText : '暂无识别'}</div>
        <div style={resultStyle}>识别耗时:{photoTime}毫秒</div>
      </div>
    </div>
  );
}

export default OcrComponent;
  1. 在PC谷歌浏览器上模拟H5测试,可以开启关闭电脑摄像头,并且拍照获取到文本电话号码4008048818,则验证成功。后续要在真机测试需要发布测试环境进一步调整。

但如果有的选择,拍照获取图片这一步还是交由原生APP去开发会好些,毕竟获取权限这一块很麻烦。非必要不放在前端,前端只做个离线识别就行。

在这里插入图片描述

  1. 最后一点,当然也是最重要的:纯前端技术实现的OCR总归还是没有付费的,以及后端服务调用API的来的好!并不是说技术受限,而是开发成本以及效率性上,有造好的轮子能满足需求,就没必要硬钢从零开始训练模型,编写识别机制啥的。

当然,如果你是超级大佬,请无视本文章。哈哈。感谢观看,可以请点赞,谢谢。


http://www.kler.cn/a/517141.html

相关文章:

  • qt QNetworkRequest详解
  • 无所不搜,吾爱制造
  • Centos 修改历史读录( HISTSIZE)
  • 安卓14自由窗口圆角处理之绘制圆角轮廓线
  • CNN-GRU卷积门控循环单元时间序列预测(Matlab完整源码和数据)
  • Hive之加载csv格式数据到hive
  • 【HarmonyOS NAPI 深度探索10】HarmonyOS Next 中的 NAPI 的架构与原理
  • U3D的.Net学习
  • 阿里云服务器在Ubuntu上安装redis并使用
  • Java 生成 PDF 文档 如此简单
  • OpenAI秘密重塑机器人军团: 实体AGI的崛起!
  • ngrok同时配置多个内网穿透方法
  • 航空航天混合动力(7)航空航天分布式电推进系统
  • ChopChopGo:一款针对Linux的取证数据快速收集工具
  • 【Unity】ScrollViewContent适配问题(Contentsizefilter不刷新、ContentSizeFilter失效问题)
  • 提升效率与体验,让笔记更智能
  • Java导出通过Word模板导出docx文件并通过QQ邮箱发送
  • 深入探索imi框架:PHP Swoole的高性能协程应用实践
  • 深入解析:Docker 容器如何实现文件系统与资源的多维隔离?
  • Go 不可重复协程安全队列
  • 全同态加密理论、生态现状与未来展望(下)
  • 华为OD机试E卷 --快递投放问题 --24年OD统一考试(Java JS Python C C++)
  • Redis Java 集成到 Spring Boot
  • 修改docker共享内存shm-size
  • 算法基础 -- 红黑树初识
  • 科家多功能美发梳:科技赋能,重塑秀发新生