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