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

## はじめに

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

## アプリケーション制作

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

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

### フロントエンドの準備

``` bash:プロジェクトのセットアップ
$ 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の準備


-  [セットアップの手順|公式docs](https://cloud.google.com/vision/docs/ocr?hl=ja#set-up-your-google-cloud-project-and-authentication)の1~3まで行う
    - GCPからプロジェクトの設定を行なう
    - VisionAPIを有効にする

![スクリーンショット 2023-12-03 17.18.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3218510/1133722c-d50b-ca5e-8058-acad1af12ad3.png)

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

![スクリーンショット 2023-12-03 17.15.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3218510/3315283b-9535-90d8-c4fb-df7d012ad361.png)

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

![スクリーンショット 2023-12-03 17.28.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3218510/555aa0f3-c045-e560-9037-662509d8bbb2.png)


### Lambdaの準備

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

##### GCPにアクセスするための設定
JsonキーをLambdaにアップロードします。
![スクリーンショット 2023-12-03 17.34.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3218510/94e71a00-34dc-47c2-fa66-ee11bc9e7f2a.png)

環境変数にこのキーのファイル名を設定します。
![スクリーンショット 2023-12-03 17.37.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3218510/c9119ca0-a7de-82f0-98de-d0008967a43e.png)

##### Lambdaレイヤーの準備

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

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

```bash:レイヤー用
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にリクエストを送って結果を表示するようにしておきました。

```Python: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
    }
```

```React: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の精度には驚かされましたね。


### 参考文献等

画像ファイル送信

https://qiita.com/Ryotaross/items/abdcb3540d0f4f9e2406

Lambda × Vision API

https://dev.classmethod.jp/articles/lambda_google-cloud-vision/

https://dev.classmethod.jp/articles/ocr-with-lambda-using-cloud-vision-api/