15
5

More than 3 years have passed since last update.

ReactNativeで湯婆婆を実装(OCR)

Last updated at Posted at 2020-11-09

はじめに

@NemesisさんのJavaで湯婆婆を実装してみる - Qiitaを発端に、様々な言語やフレームワーク、果てはサービスを題材に実装されている湯婆婆。
令和のHello World!なんて呼ばれるぐらいプログラミングの基礎問題として優れているかと思います。

「基礎的」なお題であるせいか、どの言語でも 「コマンドラインでの対話式」 で実装されていることが多いです。
記事に独自性を出すためにも、ここは原作を忠実に再現して 「入力は手書き」 そして 「入力内容をCloud Vision APIで読み取らせて例の処理を実行」 することにしました。

どうせなら指でスラスラ入力させたかったのでReactNativeで実装しています。

コード

Yubaba.jsx
import React, {useRef, useState} from 'react';
import {
  StatusBar,
  SafeAreaView,
  StyleSheet,
  Text,
  View,
  TouchableOpacity,
} from 'react-native';
import RNDrawOnScreen from 'react-native-draw-on-screen';
import ViewShot, {captureRef} from 'react-native-view-shot';

// GCPで取得したAPIキー
const GCP_API_KEY = 'xxxxxxxxxxxx';

const Yubaba = () => {
  const RNDraw = useRef(null);
  const RNViewShot = useRef(null);
  const [name, setName] = useState('');
  const newName = name.substr(Math.floor(Math.random() * name.length), 1);

  // クリアボタン押下イベント
  const onPressClear = () => {
    RNDraw?.current?.clear();
    setName('');
  };

  // OKボタン押下イベント
  const onPressOk = () => {
    // react-native-view-shotで描画部分のキャプチャを取得
    captureRef(RNViewShot, {result: 'base64'}).then(async (data) => {
      // リクエストボディ作成
      const body = JSON.stringify({
        requests: [
          {
            features: [
              {
                // テキスト認識
                type: 'TEXT_DETECTION',
                // 取得したい結果数
                maxResults: 1,
              },
            ],
            image: {
              // base64エンコードした画像をそのままセット
              content: data,
            },
          },
        ],
      });

      // CloudVisionAPI実行
      const response = await fetch(
        'https://vision.googleapis.com/v1/images:annotate?key=' + GCP_API_KEY,
        {
          headers: {
            Accept: 'application/json',
            'Content-Type': 'application/json',
          },
          method: 'POST',
          body: body,
        },
      );

      // レスポンス解析
      const resJson = await response.json();
      const description =
        resJson.responses[0]?.textAnnotations[0]?.description || '';

      // 正常にテキストが読み取れていたらsetName実行
      if (description) {
        // 改行コードが含まれていることがあるため除外
        setName(description.replace(/\r?\n/g, ''));
      }
    });
  };

  return (
    <>
      <StatusBar barStyle="dark-content" />
      <SafeAreaView style={styles.container}>
        <View style={styles.contentWrap}>
          <Text>{`契約書だよ。そこに名前を書きな。`}</Text>
        </View>
        <ViewShot style={styles.canves} ref={RNViewShot}>
          <RNDrawOnScreen penColor={'black'} strokeWidth={10} ref={RNDraw} />
        </ViewShot>
        <View style={styles.buttonWrap}>
          <TouchableOpacity
            style={[styles.button, {borderColor: '#007AFF'}]}
            onPress={onPressOk}>
            <Text style={[styles.buttonText, {color: '#007AFF'}]}>{`OK`}</Text>
          </TouchableOpacity>
          <TouchableOpacity
            style={[styles.button, {borderColor: '#FF7A00'}]}
            onPress={onPressClear}>
            <Text
              style={[styles.buttonText, {color: '#FF7A00'}]}>{`クリア`}</Text>
          </TouchableOpacity>
        </View>
        {!!name && (
          <View style={styles.contentWrap}>
            <Text>{`フン。${name}というのかい。贅沢な名だねぇ。`}</Text>
            <Text>{`今からお前の名前は${newName}だ。いいかい、${newName}だよ。\n分かったら返事をするんだ、${newName}!!`}</Text>
          </View>
        )}
      </SafeAreaView>
    </>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  contentWrap: {
    width: '100%',
    margin: '4%',
    marginTop: '8%',
  },
  canves: {
    height: '30%',
    margin: '2%',
    borderWidth: 2,
    borderColor: '#ccc',
    backgroundColor: '#FFF',
  },
  buttonWrap: {
    margin: '2%',
    width: '100%',
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
  },
  button: {
    borderWidth: 1,
    margin: '2%',
    padding: 10,
    borderRadius: 4,
  },
  buttonText: {
    fontSize: 14,
  },
});

export default Yubaba;

コードの解説

基本的な湯婆婆は@dryttさんのReact (JavaScript) で湯婆婆を実装してみる - Qiitaのロジックを踏襲しているので、
ここではこの湯婆婆の特色であるCloudVisionAPIを使った手書き文字の読み取り、について掘り下げます。

手書きUI

手書きのUIにはreact-native-draw-on-screenを使用しています。
文字認識を行いやすくするため、文字色は黒で線の太さも固定としています。

さらに手書きした内容をいったん画像として保持したいので、react-native-view-shotを使って画像化したい要素をラップしています。

import RNDrawOnScreen from 'react-native-draw-on-screen';
import ViewShot, {captureRef} from 'react-native-view-shot';

// ...略

<ViewShot style={styles.canves} ref={RNViewShot}>
  <RNDrawOnScreen penColor={'black'} strokeWidth={10} ref={RNDraw} />
</ViewShot>

キャプチャ

手書きが完了したらreact-native-view-shotでキャプチャを撮ります。
CloudVisionAPIにはbase64で送りつけたいのでオプションで指定しています。
※指定しなかった場合は端末ストレージ内に配置されたキャプチャのパスがdataに入ります。

captureRef(RNViewShot, {result: 'base64'}).then(async (data) => {/** dataにbase64エンコードした内容がそのまま入る */ }

CloudVisionAPIへ送信

あとはCloudVisionAPIの仕様に従ってリクエストを投げるだけです。
先にGCPプロジェクトを作成し、課金設定をしておく必要があります。
※1000リクエスト/月までは無料です。

 // リクエストボディ作成
const body = JSON.stringify({
  requests: [
    {
      features: [
        {
          // テキスト認識
          type: 'TEXT_DETECTION',
          // 取得したい結果数
          maxResults: 1,
        },
      ],
      image: {
        // base64エンコードした画像をそのままセット
        content: data,
      },
    },
  ],
});

// CloudVisionAPI実行
const response = await fetch(
  'https://vision.googleapis.com/v1/images:annotate?key=' + GCP_API_KEY,
  {
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    method: 'POST',
    body: body,
  },
);

実行結果

yubaba

そこは「千」じゃないんかーい!

おわりに

ここまで読んでいただきありがとうございます。
入門的とは言い難いので、湯婆婆本来の趣旨からは外れてしまったかもしれません。

Cloud Vision APIを使ったのは初めてですが、思ったよりも遥かに簡単に導入できました。
精度が落ちるかもしれませんが、カメラを起動して撮影した文字から名前を読み取ることもできます。

15
5
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
15
5