React

【ReactNative+CloudVision】「怒り顔採点アプリ」を作った

いつもご利用ありがとうございます。このブログは、広告費によって運営されています。

オススメ本
Web技術を勉強するなら、かなりオススメの雑誌です。毎月新しい発見があります。ついに最終号・・・、みなさん買いましょう!!
読んで損することはない名著。命名で悩むことが多い人はこの本がオススメです。

ReactNative が一番自分に合っていると思って、アプリを作ってみました。

今回できるようにすることの目標

・カメラを使う

・撮った画像の圧縮、利用

・画像の解析 API を使う

・UI を覚える

っていうのを考えていて、1週間で形にしてビルドする!までを目標にしました。

 

トップページは他のアプリを参考に

ナビゲーターとグラデーションのライブラリを使って、ボタンは ReactNativeElement を使いました。

OKORIGAOimage1

スタートすると、名前だけ入力して開始。

okorigaoimage5

名前入力するとカメラの許可を求められて、カメラが起動する

カメラが写す部分の表示の大きさは変えられるし、色々メソッドが用意されていたのでそれを色々試しながらやったけど実機で試さないといけないので時間もかかるのであんまりいじってないです。

自撮りモードにするのとかはすごく簡単に実装できます。

okorigaoimage2

 

スコアを表示する画面

API から送られてくる情報をもとに計算して結果を表示しています。

okorigaoimage3

 

過去に撮った写真は見直せるようにしました。

okorigaoimage4

このアプリは高校生や大学生が罰ゲームを決めるときに変顔して点数つけて、あとで見返して超面白い顔、みたいなことを考えながら作りました。

アプリを作った時の流れ

Navigator を整える →TOP ページのコーディング → カメラ撮影 → 圧縮と API と保存 → 形を整える

といった感じになりました。

以下はコードとかの話になります。

パッケージを触ったことがある人にしかわからない話ばかりになっちゃうと思います。

自分は npm しか使ったことがなくて、yarn とか全くわかりません。差もわかりません。

使ったパッケージ

    "@expo/vector-icons": "^10.0.0",
    "axios": "^0.19.0",
    "expo": "^35.0.0",
    "expo-camera": "~7.0.0",
    "expo-linear-gradient": "~7.0.0",
    "expo-permissions": "~7.0.0",
    "firebase": "^7.5.0",
    "native-base": "^2.13.8",
    "react": "16.8.3",
    "react-dom": "16.8.3",
    "react-native": "https://github.com/expo/react-native/archive/sdk-35.0.0.tar.gz",
    "react-native-elements": "^1.2.7",
    "react-native-gesture-handler": "~1.3.0",
    "react-native-paper": "^3.2.1",
    "react-native-reanimated": "~1.2.0",
    "react-native-web": "^0.11.7",
    "react-navigation": "^4.0.10",
    "react-navigation-material-bottom-tabs": "^2.1.5",
    "react-navigation-stack": "^1.10.3",
    "react-navigation-tabs": "^2.6.0",
    "expo-image-manipulator": "~7.0.0",
    "expo-face-detector": "~7.0.0",
    "expo-media-library": "~7.0.0"

ReactNavigation を使う

    "react-navigation": "^4.0.10",
    "react-navigation-stack": "^1.10.3",
    "react-navigation-tabs": "^2.6.0",</code></pre>

⇨【簡単にテンプレで実装】react-navigation を導入してみた

上記の記事で使い方はだいたいわかると思います。

アイコンを使ってタブメニューを世の中にあるアプリっぽく作りました。

トップページの色合いについては、自分が最初に作ったものとは実は違っていて、

「色どうでしょう・・・?」という風に他の人に聞いたら、アドバイスをくれました。

人には聞いてみるべきだなって改めて思いました。

import { createAppContainer } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';
import { createBottomTabNavigator } from 'react-navigation-tabs';

現在のバージョンでは3つインポートして使います。昔はまとめてインポートしたみたいですが現在はパケージが別れています。

const ContentStack = createStackNavigator(
  {
    Front: { screen: FrontScreen }, //ニックネーム入力するところ
    Home: { screen: HomeScreen }, //メインコンテンツ。膨大。コンポーネント化させたいが時間がない妥協
    Content: { screen: ContentScreen } //いらない説
  },
  {
    defaultNavigationOptions: {
      headerTitle: '怒り顔採点アプリ',
      headerTintColor: 'black'
    }
  }
);

タブの数だけスタックナビゲーターを作成しておいて、以下の方法で呼ぶ

export default createAppContainer(
  createBottomTabNavigator(
    {
      Top: {
        screen: TopStack,
        navigationOptions: {
          tabBarIcon: () =&gt; &lt;FontAwesome5 name='home' size={24} /&gt;
          // tabBarVisible: false
        }
      },
      Content: {
        screen: ContentStack,
        navigationOptions: {
          tabBarIcon: () =&gt; &lt;FontAwesome5 name='camera' size={24} /&gt;
        }
      },
      Photo: {
        screen: PhotoStack,
        navigationOptions: {
          tabBarIcon: () =&gt; &lt;FontAwesome5 name='images' size={24} /&gt;
        }
      },
      Settings: {
        screen: SettingsStack,
        navigationOptions: {
          tabBarIcon: () =&gt; &lt;FontAwesome5 name='cogs' size={24} /&gt;
        }
      }
    },
    {
      // You can return any component that you like here!
      //ここまでヘッダーとか
      initialRouteName: 'Top', //本当はTop
      tabBarOptions: {
        showLabel: false,
        inactiveBackgroundColor: '#eceff1',
        activeBackgroundColor: '#fff'
      } //以下メニュー
    }

グラデーション LinearGradient を使う

"expo-linear-gradient": "~7.0.0"
import { LinearGradient } from'expo-linear-gradient';

~~~~
        <LinearGradient
              colors={['#05fbff', '#1d62f0']}
              style={{
                flex: 1,
                width: '100%',
                justifyContent: 'center',
                alignItems: 'center'
              }}
            >
                //この中にコンテンツ
         </LinearGradient>

青の背景はこれで作っています。

expo-camera を設置する

"expo-camera": "~7.0.0"

結果としてこんなに簡単なのかってビックリした。

import { Camera } from 'expo-camera';
~~~~~
<Camera
   style={{ flex:1 }}
   type={this.state.type}
   ref={ref=&gt; {
   this.camera = ref;
 }}
 >
//この中にボタンとか設置するViewも入れられるしなんでも放り込めそう。自由。
</Camera>

expo-permission でカメラの許可をとる

"expo-permissions": "~7.0.0"

カメラを使用する画面の冒頭でカメラ使用の許可をとります。

import * as Permissions from 'expo-permissions';

class HomeScreen extends React.Component {
  state = {
    hasCameraPermission: null
}

asynccomponentDidMount() {
   const { status } =awaitPermissions.askAsync(Permissions.CAMERA);
   this.setState({
        hasCameraPermission:status==='granted'
    });
  }
~~~~~

  render() {
    const {
      hasCameraPermission,  //毎回this.stateを書くのは面倒なのでこれを書くと省略することができる
   } = this.state;



if (hasCameraPermission===null) {
return<View/>;
    } elseif (hasCameraPermission===false) {
return<Text>No access to camera</Text>;
    } else {
return
//以下にカメラでやりたいことを書く

許可が取れたら先ほどのカメラのコードを return するようにして書けば良いです。

takePictureAsync()で写真を取る

びっくりするほど簡単に写真を撮影して、スマホのストレージに一時保存させることができます。

PHP でも画像を Files で Post するとサーバーに一時保存させれると思いますが、そんな感じ。

onPress するメソッドにこんな感じで書けば良いです。

snapShot = async () => {
    let result = await this.camera.takePictureAsync(); //この1行で写真撮れる
}

この result に中に写真の情報が全て入ってる。ぜひコンソールで叩いてみてください。

result.uri をコンソールすると一時保存されている場所がわかるのでそこを呼び出して image コンポーネントに source で与えれば今まさに撮ったばかりの写真を表示できます。

カメラ起動画面にボタンを設置して onPress でこの snapShot を着火させます。

前述のカメラコンポーネント内に、以下を記述していただければ大丈夫です。

React-Native-Elements を使っているので、パッケージをインストールする必要があります。

"react-native-elements": "^1.2.7"
 <View style={styles.camera}>
    <TouchableOpacity //写真をとるボタン
      style={styles.cameraTouch}
      onPress={() => {
        this.loading();
        this.snapShot();
      }}
    >
      <Icon
        name='adjust'
        color='#fff'
        size={90}
      />
    </TouchableOpacity>
    <TouchableOpacity //ここから下はカメラの反転用のアイコンです。
      onPress={() => {
        this.toggleCamera();
      }}
    >
      <Icon
        style={styles.cameraTouch2}
        name='cached'
        type='material'
        color='#fff'
        size={60}
      />
    </TouchableOpacity>
  </View>
~~~~~~~~

  camera: {
    flex: 1,
    backgroundColor: 'transparent',
    flexDirection: 'row'
  },
  cameraTouch: {
    flex: 1,
    alignSelf: 'flex-end',
    alignItems: 'center',
    marginBottom: 10
  },
  cameraTouch2: {
    flex: 1,
    alignSelf: 'flex-end',
    alignItems: 'center',
    marginBottom: 10
  },

expo-image-manipulator を使って画像を圧縮する

"expo-image-manipulator": "~7.0.0"

これから送信する GoogleVisionAPI に送れる画像は 1  base64 形式 2 1 M バイト以下?(詳しい数値が思い出せない) という制約があって、実はこの容量オーバーであるというエラーで跳ね返されているというのを把握するのに時間がかかりました。 こういった悩みを解決するためにこのパッケージを使って圧縮しました。

import * as ImageManipulator from 'expo-image-manipulator';

~~~~~~~~~
     const photo = await ImageManipulator.manipulateAsync(
        result.uri,
        actions,
        {
          compress: 0.4,
          base64: true
        }
      );

これで photo に圧縮された上に base64 形式に変換されたものが出来上がります。

ぜひ console で photo を叩いてみてください。

Google の Cloud Vision API で顔の表情の解析をする

表情解析結果を取得して、点数をつけて自身のサクラサーバー内に設置した API を通して MySQL に結果を保存しています。

コピペでは使えない部分は、APIkey と自分のサーバーのところの URI だけだと思います。

checkImage = async photo => {
    // console.log(photo);
    const body = JSON.stringify({
      requests: [
        {
          features: [
            {
              type: 'FACE_DETECTION',
              maxResults: 1
            }
          ],
          image: {
            content: photo.base64
          }
        }
      ]
    });
    const response = await fetch(
      'https://vision.googleapis.com/v1/images:annotate?key=ここにAPIkeyを入れてください', // ''は不要
      {
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/json'
        },
        method: 'POST',
        body: body
      }
    );
    this.setState({
      showResult: true
    });
    const Json = await response.json();

//このJsonに全ての帰ってきたデータがオブジェクトで入ってる。
//以下で帰ってきたデータを加工するだけ。この加工が上手ければ本当に素晴らしいものが作れると思います。一番大事な部分だと思う。
    if (Json.responses[0]) {
      const face = Json.responses[0]['faceAnnotations'][0];
      this.setState({
        result: face
      });
      console.log(this.state.result);

      const angry_point = face.angerLikelihood;
      const left_top = face.landmarks[16].position.y; //左目上
      const left_bottom = face.landmarks[18].position.y; //左目下
      const right_top = face.landmarks[21].position.y; //右目上
      const right_bottom = face.landmarks[23].position.y; //右目下
      const left_height = left_bottom - left_top;
      const right_height = right_bottom - right_top;
      angry_point === 'VERY_LIKELY'
        ? this.setState({ point: this.state.point + 70 })
        : this.setState({ point: this.state.point + 40 });
      left_height + right_height &gt; 60
        ? this.setState({ point: this.state.point + 30 })
        : this.setState({
            point:
              this.state.point +
              Math.ceil(((left_height + right_height) / 2) * 100) / 100 //少数第二位で繰り上げ
          });
    } else {
      const resultScreen = 'AIが顔を認識しませんでした';
      Alert.alert(resultScreen);
    }

    let qs = require('qs');
    axios.post(
      'http://あなたのサーバーのAPIをここに!',
      qs.stringify({
        name: this.state.name,
        response: response,
        point: this.state.point
      })
    );

おそらくブログ読みながら進めていれば1時間程度でここまでたどり着けると思います。

俺は1週間かかった!

ガンガン真似して向上させたパワーアップバージョンをネット上に公開してくださいよろしくお願いします。

デプロイまでは後半戦の記事で書きたいと思います。

ここまでのまとめ

断片的な情報しかない ReactNative で1つのアプリを作り上げるということがめちゃくちゃ大変であることが分かりました。

特に今回はカメラというネイティブアプリでは避けては通れないものを使えて非常に満足です。

ただ、1つ間違い無く言えるのは ReactNative は、Javascript でアプリを作れてしまうということ。

本気でデータベースを使い始めると Java か kotlin に手を出さないといけないかもしれないですが、基本的に JS でできてしまうはずです。

これはマジですごいことだと思います。

自分が今回詰まった場所も、ほぼ JS の同期非同期問題と、Http 通信と、ファイルの形式と、ほぼ JS のレベルがまだまだ低いことが原因でした。

ReactNative が難しそうと思われる原因は、覚えるコンポーネントが多すぎること、ツールやパッケージの知識が必要不可欠であることだと思います。全てのツールやパッケージのバージョンアップに対応させるのは骨だと思います。

ただ ReactNative は面白い。それは間違いないと思います。

自分がなんと無く使っているアプリは1つのボタンにも影が付いているし、押したら凹むし、ページ遷移のスライドが右からだったり左からだったりするし、奥からちかづくように浮いてくるときもあるし、

マジでリスペクトが止まらんっす。