React Native Cameraで撮影後に顔検出する

## プレビュー時じゃなくて撮影後に顔検出したい
仕事でReact Nativeを使ってカメラで撮影した画像から顔認識の教師用データをサーバーにアップロードする必要があったのですが、React Native Cameraを使えば顔検出までクライアント側でできることがわかり使ってみました。
サーバー側で顔検出を行ってもよいのですが、クライアント側でできれば顔が検出できなかったときのフィードバックも楽です。
[公式ドキュメント](https://react-native-community.github.io/react-native-camera/docs/rncamera)にはカメラのシャッターを切る前のプレビューにて顔が検出されたときのコールバック関数を指定できるオプションは書いてあるのですが、撮影後の画像に対して顔検出する方法は記載がありませんでした。
なんかいい方法はないのかと探してみるとdetectFacesAsyncというメソッドがあり、これを使えば撮影後の画像に対して顔検出できることがわかりました。諦めなくてよかった！
[Feat. Request: use ML Kit face detector for single file](https://github.com/react-native-community/react-native-camera/issues/2277)
React Native CameraはExpo Cameraがベースだそうで、Expoの方にはこのメソッドについて記載されていました。
[FaceDetector](https://docs.expo.io/versions/latest/sdk/facedetector/)
こんな感じで使います。

```react
import { FaceDetector} from 'react-native-camera';
...
const img = await FaceDetector.detectFacesAsync(uri, options);
```
uriには対象とする画像のuriを指定します。optionsにはmode(速さ優先か精度優先か), detectLandmarks(顔のランドマーク検出を行うか), runClassifications(目の開閉など追加の判定を行うか)を指定できます。
返ってくるオブジェクトには顔領域の左上の始点座標と、幅、高さなどの情報が含まれます。
スマホのカメラで撮影した画像はかなり大きい（手持ちのPixel 3aだと3000x4000pixel程度）ため、顔検出しようとすると10秒くらいかかりました。
気にならない時間内で検出するには画像をリサイズしてから顔検出にかける必要があります。
## サンプルプログラム
1. 撮影
2. リサイズ
3. 顔検出
4. 元画像から顔領域を切り出してモーダルで表示  

するサンプルを作ってみました。
リサイズにはreact-native-image-resizer,クロップにはreact-native-communityのimage-editorを使用しています。

```react
import React, { PureComponent } from 'react';
import { AppRegistry, StyleSheet, Modal, Text, TouchableOpacity, View, Button, Image } from 'react-native';
import { RNCamera, FaceDetector } from 'react-native-camera';
import ImageEditor from "@react-native-community/image-editor";
import ImageResizer from 'react-native-image-resizer';

class Index extends PureComponent {
    constructor(props) {
        super(props);
        this.state = { isModalVisible: false, imageUri: null}
    }
    render() {
        return (
            <View style={styles.container}>
                <RNCamera
                    ref={ref => {
                        this.camera = ref;
                    }}
                    style={styles.preview}
                    type={RNCamera.Constants.Type.back}
                    androidCameraPermissionOptions={{
                        title: 'Permission to use camera',
                        message: 'We need your permission to use your camera',
                        buttonPositive: 'Ok',
                        buttonNegative: 'Cancel',
                    }}
                />
                <View style={{flex: 0, flexDirection: 'row', justifyContent: 'center'}}>
                    <TouchableOpacity onPress={this.takePicture.bind(this)} style={styles.capture}>
                        <Text style={{fontSize: 14}}> SNAP </Text>
                    </TouchableOpacity>
                </View>
                <Modal visible={this.state.isModalVisible}>
                    <View style={{flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: "#fff"}}>
                        <Image style={{width: 300, height: 300}}
                               source={{uri: this.state.imageUri}}
                               resizeMode="contain"
                        />
                        <Button
                            title="OK"
                            onPress={() => {this.setState({isModalVisible: false})}}
                        />
                    </View>
                </Modal>
            </View>
        );
    }

    takePicture = async () => {
        if (this.camera) {
            const data = await this.camera.takePictureAsync();
            // 顔検出の時間短縮のためリサイズする
            const resizedImage = await ImageResizer.createResizedImage(data.uri, 450, 600, 'JPEG', 100);
            const scale = 3024 / 450;
            // 顔検出を行う
            const faceData = await FaceDetector.detectFacesAsync(resizedImage.uri);
            const faces = faceData.faces;
            if (faces.length > 0) {
                // 最初に検出した顔を元画像から切り抜いてキャッシュに保存する
                const cropData = {
                    // 顔検出は縦向きの画像に対して始点の座標とサイズ情報が返ってくるが、クロップは横向きでの座標のため90°変換が必要
                    // スケールも元に戻す
                    offset: {
                        x: faces[0].bounds.origin.y * scale,
                        y: data.height - (faces[0].bounds.size.width + faces[0].bounds.origin.x) * scale
                    },
                    size: {width: faces[0].bounds.size.height * scale, height: faces[0].bounds.size.width * scale},
                };
                const uri = await ImageEditor.cropImage(data.uri, cropData);
                this.setState({isModalVisible: true, imageUri: uri});
            }
        };
    }
}
const styles = StyleSheet.create({
    container: {
        flex: 1,
        flexDirection: 'column',
        backgroundColor: 'black',
    },
    preview: {
        flex: 1,
        justifyContent: 'flex-end',
        alignItems: 'center',
    },
    capture: {
        flex: 0,
        backgroundColor: '#fff',
        borderRadius: 5,
        padding: 15,
        paddingHorizontal: 20,
        alignSelf: 'center',
        margin: 20,
    },
});

AppRegistry.registerComponent('App', () => Index);

```
## 実行画面
サンプルをAndroid実機で動かした画面はこんな感じです。
撮影画面
<img width="200" alt="Screenshot_20191211-215922.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/425095/3c6e48cf-4653-afd9-8c5b-4674885fa992.png">
SNAPを押すと顔検出してクロップ
<img width="200" alt="Screenshot_20191211-215934.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/425095/ae20727e-c09e-f556-ffd5-334f9232338f.png">

ちなみに撮影用の顔はこちらのサイトで生成しました。
https://thispersondoesnotexist.com/
実在しない顔を無限に生成できます。

同じようなことをしたい人に役立てば幸いです。