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