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