ホーム
Sponserd by↑転職したい人向け、ベンチャー企業の採用動画があります

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

11/28日〜12/5日に掛けてG’s AcademyのPHP選手権がありました。

前回のJS選手権では、Nuxt×Firebaseで表参道のランチマップを作成しました。

https://qiita.com/mattun_evo/items/670aa8e7ef0b6737097d

自分はその前の1週間カリキュラムと並行して、ReactNativeをずっと触っていたので、

今回の選手権ではReactNativeで大部分を作成して、自身のサーバーに置いたPHPをAPIみたいな使い方をしてMySQLに保存するという感じでやることにしました。

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

・カメラを使う

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

・画像の解析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>

https://poppotennis.com/react_native_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 1Mバイト以下?(詳しい数値が思い出せない) という制約があって、実はこの容量オーバーであるというエラーで跳ね返されているというのを把握するのに時間がかかりました。 こういった悩みを解決するためにこのパッケージを使って圧縮しました。

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つのボタンにも影が付いているし、押したら凹むし、ページ遷移のスライドが右からだったり左からだったりするし、奥からちかづくように浮いてくるときもあるし、

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