1import * as FaceDetector from 'expo-face-detector';
2import * as ImagePicker from 'expo-image-picker';
3import React from 'react';
4import { Alert, Image, PixelRatio, Platform, ScrollView, StyleSheet, View } from 'react-native';
5
6import { scaledFace, scaledLandmarks } from '../components/Face';
7import ListButton from '../components/ListButton';
8import MonoText from '../components/MonoText';
9
10interface State {
11  selection?: ImagePicker.ImagePickerAsset;
12  faceDetection?: {
13    detecting: boolean;
14    faces: FaceDetector.FaceFeature[];
15    image?: FaceDetector.Image;
16    error?: any;
17  };
18}
19
20const imageViewSize = 300;
21
22export default class FaceDetectorScreen extends React.Component<object, State> {
23  static navigationOptions = {
24    title: 'FaceDetector',
25  };
26
27  readonly state: State = {};
28
29  detectFaces = (picture: string) => {
30    this.setState({
31      faceDetection: {
32        detecting: true,
33        faces: [],
34      },
35    });
36    FaceDetector.detectFacesAsync(picture, {
37      mode: FaceDetector.FaceDetectorMode.accurate,
38      detectLandmarks: FaceDetector.FaceDetectorLandmarks.all,
39      runClassifications: FaceDetector.FaceDetectorClassifications.none,
40    })
41      .then((result) => {
42        this.setState({
43          faceDetection: {
44            detecting: false,
45            faces: result.faces,
46            image: result.image,
47          },
48        });
49      })
50      .catch((error) => {
51        this.setState({
52          faceDetection: {
53            detecting: false,
54            faces: [],
55            error,
56          },
57        });
58      });
59  };
60
61  showPicker = async (mediaTypes: ImagePicker.MediaTypeOptions, allowsEditing = false) => {
62    const { granted } = await ImagePicker.requestMediaLibraryPermissionsAsync();
63    if (granted || Platform.OS === 'web') {
64      const result = await ImagePicker.launchImageLibraryAsync({
65        mediaTypes,
66        allowsEditing: true,
67      });
68      if (result.canceled) {
69        this.setState({ selection: undefined });
70      } else {
71        const [asset] = result.assets;
72        this.setState({ selection: asset });
73        this.detectFaces(asset.uri);
74      }
75    } else {
76      Alert.alert('Permission required!', 'You must allow accessing images in order to proceed.');
77    }
78  };
79
80  render() {
81    return (
82      <ScrollView style={{ padding: 10 }}>
83        <ListButton
84          onPress={() => {
85            this.showPicker(ImagePicker.MediaTypeOptions.Images);
86          }}
87          title="Pick photo"
88        />
89        {this._maybeRenderSelection()}
90        {this._maybeRenderFaceDetection()}
91      </ScrollView>
92    );
93  }
94
95  _maybeRenderSelection = () => {
96    const { selection } = this.state;
97
98    if (!selection) {
99      return;
100    }
101
102    return (
103      <View style={styles.sectionContainer}>
104        {!selection || selection.type === 'video' ? null : (
105          <View style={styles.imageContainer}>
106            <Image source={{ uri: selection.uri }} resizeMode="contain" style={styles.image} />
107            {this._maybeRenderDetectedFacesAndLandmarks()}
108          </View>
109        )}
110        <MonoText>{JSON.stringify(selection, null, 2)}</MonoText>
111      </View>
112    );
113  };
114
115  _maybeRenderFaceDetection = () => {
116    const { selection, faceDetection } = this.state;
117
118    if (!selection || !faceDetection) {
119      return;
120    }
121
122    if (faceDetection && faceDetection.detecting) {
123      return (
124        <View style={styles.sectionContainer}>
125          <MonoText>Detecting faces…</MonoText>
126        </View>
127      );
128    }
129
130    if (faceDetection && faceDetection.error) {
131      return (
132        <View style={styles.sectionContainer}>
133          <MonoText>Something went wrong: {JSON.stringify(faceDetection.error)}</MonoText>
134        </View>
135      );
136    }
137
138    if (faceDetection && !faceDetection.detecting) {
139      return (
140        <View style={styles.sectionContainer}>
141          <MonoText>Detected faces: {JSON.stringify(faceDetection.faces)}</MonoText>
142          {faceDetection.image && (
143            <MonoText>In image: {JSON.stringify(faceDetection.image)}</MonoText>
144          )}
145        </View>
146      );
147    }
148
149    return null;
150  };
151
152  _maybeRenderDetectedFacesAndLandmarks = () => {
153    const { selection, faceDetection } = this.state;
154    if (selection && faceDetection) {
155      const { pixelsToDisplayScale } = calculateImageScale(selection);
156      return (
157        <View
158          style={{
159            ...imageOverflowSizeAndPosition(selection),
160            position: 'absolute',
161          }}>
162          {this.state.faceDetection &&
163            this.state.faceDetection.faces.map(scaledFace(pixelsToDisplayScale))}
164          {this.state.faceDetection &&
165            this.state.faceDetection.faces.map(scaledLandmarks(pixelsToDisplayScale))}
166        </View>
167      );
168    }
169    return null;
170  };
171}
172
173const imageOverflowSizeAndPosition = (image: ImagePicker.ImagePickerAsset) => {
174  const { scaledImageWidth, scaledImageHeight } = calculateImageScale(image);
175  return {
176    top: (imageViewSize - scaledImageHeight) / 2,
177    left: (imageViewSize - scaledImageWidth) / 2,
178    width: scaledImageWidth,
179    height: scaledImageHeight,
180  };
181};
182
183const calculateImageScale = (image: ImagePicker.ImagePickerAsset) => {
184  let scale = 1;
185  const screenMultiplier = PixelRatio.getPixelSizeForLayoutSize(1);
186  const imageHeight = image.height / screenMultiplier;
187  const imageWidth = image.width / screenMultiplier;
188  if (imageWidth > imageHeight) {
189    scale = imageViewSize / imageWidth;
190  } else {
191    scale = imageViewSize / imageHeight;
192  }
193  return {
194    displayScale: scale,
195    pixelsToDisplayScale: scale / screenMultiplier,
196    scaledImageWidth: imageWidth * scale,
197    scaledImageHeight: imageHeight * scale,
198  };
199};
200
201const styles = StyleSheet.create({
202  sectionContainer: {
203    marginVertical: 16,
204    justifyContent: 'center',
205    alignItems: 'center',
206  },
207  imageContainer: {
208    marginBottom: 10,
209    width: imageViewSize,
210    height: imageViewSize,
211  },
212  image: { flex: 1, width: imageViewSize, height: imageViewSize },
213});
214