LoginSignup
6
3

React+ Lambda + Vision APIで日本語文字検出アプリ

Last updated at Posted at 2023-12-03

はじめに

画像ファイルの日本語文字認識のデモアプリを制作しました。
日本語のOCRをするにあたって、精度やスピードはVision APIがダントツらしいので実装してみました。

アプリケーション制作

フロントエンドは、個人的に馴染みのあるReactを選びました。

バックエンドは、拡張性も想定してLambdaを使うことにしました。
ただAmplifyを使ったり、API Gatewayを設置したりすると手間がかかりすぎると思った+後から修正できるはずなので、とりあえず関数URLを設定したLambdaのみを作って、動くところまで実装しています。

フロントエンドの準備

プロジェクトのセットアップ
$ npm create vite@latest
Need to install the following packages:
create-vite@5.0.0
Ok to proceed? (y)
✔ Project name: … vision-api-demo
✔ Select a framework: › React
✔ Select a variant: › TypeScript

$ cd vision-api-demo
$ npm install
$ npm run dev

npm run devでローカルにアプリケーションが立ち上がれば大丈夫です。

Vision APIの準備

スクリーンショット 2023-12-03 17.18.23.png

サービスアカウントを作成

サービスアカウントのロールは「オーナー」にします。

スクリーンショット 2023-12-03 17.15.36.png

サービスアカウントに紐づくJsonキーを作成してダウンロードしておきます。

スクリーンショット 2023-12-03 17.28.35.png

Lambdaの準備

今回はマネージコンソールからLambdaだけを作成しました。
実行環境はPython3.10で構築しています。

GCPにアクセスするための設定

JsonキーをLambdaにアップロードします。
スクリーンショット 2023-12-03 17.34.44.png

環境変数にこのキーのファイル名を設定します。
スクリーンショット 2023-12-03 17.37.57.png

Lambdaレイヤーの準備

PythonからGCPにアクセスするライブラリをインストールしたレイヤーを作成します。

Lambdaと同じランタイム環境でインストールを行った方が良いので、Cloud9で以下のコマンドを使ってzipファイルを作成します。

レイヤー用
mkdir google-cloud-vision
cd google-cloud-vision
mkdir python
pip3 install -t ./python urlib3<2
pip3 install -t ./python google-cloud-vision
zip -r google-cloud-vision.zip python

補足:urllib3のバージョンが2.xだとLambdaでエラーが発生するので、バージョン指定してます。

バックエンドとフロントエンドの連携

Lambdaは、画像を受け取ってVision APIにリクエストを送り結果を返す処理を書いています。
フロントは画像をアップロードしてLambdaにリクエストを送って結果を表示するようにしておきました。

lambda_function.py
import json

import base64
import boto3
from google.cloud import vision

def lambda_handler(event, context):
    content = event['body']
    img_bytes = base64.b64decode(content)
    img_binary = base64.b64decode(img_bytes)
    
    
    # Cloud Vision API呼び出し
    client = vision.ImageAnnotatorClient()
    image = vision.Image(content=img_binary)
    response = client.document_text_detection(
        image=image,
        image_context={'language_hints': ['ja']}
    )
    
    # レスポンスからテキストデータを抽出
    output_text = ''
    for page in response.full_text_annotation.pages:
        for block in page.blocks:
            for paragraph in block.paragraphs:
                for word in paragraph.words:
                    output_text += ''.join([
                        symbol.text for symbol in word.symbols
                    ])
                output_text += '\n'

    return {
        'statusCode': 200,
        'result':"SUCCESS",
        'output_text':output_text
    }
App.tsx
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Container from '@mui/material/Container';
import CssBaseline from '@mui/material/CssBaseline';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import axios from "axios";
import { useState } from 'react';
import './App.css';


function App() {
  const defaultTheme = createTheme();
  const [imageData, setImageData] = useState(null)
  const [base64Image, setBase64Image] = useState(null)

  // ここを書き換える
  const functionURL = "https://xxxxxxx.lambda-url.ap-northeast-1.on.aws/"

  // ファイル選択時のイベント
  function onFileChange(input) {
      const files = input.target.files
      if (files.length > 0) {
          var file = files[0]
          var reader = new FileReader()
          reader.onload = (event) => {
            setImageData(event.target.result)
            setBase64Image(event.target.result.split(',')[1])
          };
          reader.readAsDataURL(file)
      } else {
        setImageData(null)
      }
  }

  // 画像プレビューの定義
  let preview
  if (imageData != null) {
    preview = (
        <div>
            <img src={imageData} width="100%"/>
        </div>
    )
  }

  // OCRリクエスト
  const invoke = async() => {
    const response = await axios.post(functionURL, base64Image)
    console.log(response)
  }

  return (
    <>
      <ThemeProvider theme={defaultTheme}>
      <Box
        sx={{
          display: 'flex',
          flexDirection: 'column',
          minHeight: '100vh',
        }}
      >
        <CssBaseline />
        <Container component="main" sx={{ mt: 8, mb: 2 }} maxWidth="sm">
          <Typography variant="h2" component="h1" gutterBottom>
            文字認識アプリ
          </Typography>
          <Typography variant="h5" component="h2" gutterBottom>
          <input type="file" accept="image/*" onChange={(e) => {onFileChange(e)}} />
          <Button variant="contained" onClick={invoke}>
            Submit
          </Button>
          </Typography>
          <Typography variant="body1">
                {preview}
          </Typography>
        </Container>
        <Box
          component="footer"
          sx={{
            py: 3,px: 2,mt: 'auto',
            backgroundColor: (theme) =>
            theme.palette.mode === 'light'
              ? theme.palette.grey[200]
              : theme.palette.grey[800],
          }}
        >
          <Container maxWidth="sm">
            <Typography variant="body1">
              My custom footer
            </Typography>
          </Container>
        </Box>
      </Box>
    </ThemeProvider>
    </>
  )
}

export default App

おわりに

無事にバックエンドで画像を受け取って文字認識するところまでできました。
Vision APIの精度には驚かされましたね。

参考文献等

画像ファイル送信

Lambda × Vision API

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3