반응형
해당 글은 React-native 환경에서 Tensorflow.js의 blazeface 모델을 이용하여 얼굴을 감지하는 것에 대한 주제로 기초 환경설정하는 부분에서부터 개발과정에 대해서 설명하는 글로 작성하였습니다.
1) 기초 환경 설정
1. 프로젝트 생성
해당 프로젝트는 React-native의 'expo-cli' 개발 방법을 선택하였으며 Typescript 기반으로 환경을 구성하였습니다.
# expo-cli 기반 React Native TypeScript 프로젝트 생성
$ expo init rn-tfjs-face-detection -t expo-template-blank-typescript
2. 라이브러리 구성
해당 라이브러리 구성은 얼굴 감지를 위한 Tensorflow.js 관련 라이브러리를 설치하였으며, 이외 필요한 라이브러리와 측정을 하기 위한 expo-camera와 얼굴의 표시를 위한 react-native-canvas를 사용하였습니다.
"dependencies": {
"@react-native-async-storage/async-storage": "^1.16.1",
"@tensorflow-models/blazeface": "^0.0.7",
"@tensorflow/tfjs": "^3.14.0",
"@tensorflow/tfjs-backend-webgl": "^3.14.0",
"@tensorflow/tfjs-converter": "^3.14.0",
"@tensorflow/tfjs-core": "^3.14.0",
"@tensorflow/tfjs-react-native": "^0.8.0",
"@types/react-native-canvas": "^0.1.8",
"expo": "~44.0.0",
"expo-camera": "^12.1.2",
"expo-gl": "^11.1.2",
"expo-status-bar": "~1.2.0",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-native": "0.64.3",
"react-native-canvas": "^0.1.38",
"react-native-fs": "^2.19.0",
"react-native-web": "0.17.1",
"react-native-webview": "^11.17.2"
},
3. 라이브러리의 플랫폼 호환성
해당 글에서 사용하는 ‘expo-camera’는 특정 플랫폼에 대해서만 호환이 됩니다.
[출처] Expo Documentation
2) 개발 과정
1. 디바이스의 카메라 접근 권한 허용
앞으로 사용하게 될 TensorCamera에 대해서 '카메라에 대해 접근 허용'을 받아야 수행할 수 있기에 선행작업으로 추가합니다.
useEffect(() => {
(async () => {
await fn_requestPermisison();
})()
}, [])
/**
* 카메라 권한 요청 함수
*/
const fn_requestPermisison = async () => {
const { status } = (await Camera.requestCameraPermissionsAsync())
if (status !== 'granted') {
console.log("카메라의 권한 요청이 승인 되지 않았습니다.")
return;
}
};
2. TensorCamera 구성
해당 부분은 공식 사이트 문서에 작성된 기반으로 작성을 하였습니다. 이는 expo-camera를 통해서 TensorCamera를 구성하고 이를 통해 반환받는 TensorImage를 얻기 위한 목적으로 구성하였습니다.
[참고] Tensorflow.js 공식 사이트
import { createRef } from 'react';
import { Platform, StyleSheet, View } from 'react-native';
import { Camera } from "expo-camera";
import { cameraWithTensors } from "@tensorflow/tfjs-react-native";
/**
* TensorflowCamera를 통해 얼굴을 측정하는 컴포넌트
* @returns
*/
const App: React.FC = () => {
// expo-camera를 통해서 TensorCamera를 구성한다.
const TensorCamera = cameraWithTensors(Camera);
// TensorCamera의 엘리먼트 정보를 가져온다.
const tensorCameraRef = createRef<any>();
// TensorCamera의 미리보기 운영체제 별 너비/높이 지정
const textureDims = Platform.OS === "ios" ? { height: 1920, width: 1080 } : { height: 1200, width: 1600 };
// 원하는 카메라의 사이즈를 지정한다.
const CAMERA_SIZE = { height: 480, width: 320 };
useEffect(() => {
(async () => {
await fn_requestPermisison();
})()
}, [])
/**
* 카메라 권한 요청 함수
*/
const fn_requestPermisison = async () => {
const { status } = (await Camera.requestCameraPermissionsAsync())
if (status !== 'granted') {
console.log("카메라의 권한 요청이 승인 되지 않았습니다.")
return;
}
};
/**
* TensorCamera가 특정 시간 마다 루프를 돌면서 측정된 값을 반환 해줌.
* @param images : 카메라 이미지를 나타내는 텐서를 생성
* @param updatePreview : WebGL 렌더 버퍼를 카메라의 내용으로 업데이트하는 함수
* @param gl : 렌더링을 수행하는 데 사용되는 ExpoWebGl 컨텍스트
*/
const fn_onReadyTensorCamera = async (images: any, updatePreview: any, gl: any) => {
const loop = async () => {
// TensorCamera에서 루프를 돌면서 나온 텐서 이미지
const nextImageTensor = images.next().value;
// 2초간 반복적으로 루프를 반복한다.
setTimeout(() => {
requestAnimationFrame(loop);
}, 2000);
}
loop();
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
camera: {
position: "absolute",
width: CAMERA_SIZE.width,
height: CAMERA_SIZE.height,
},
});
return (
<View style={styles.container}>
<TensorCamera
ref={tensorCameraRef} // 엘리먼트 정보
style={styles.camera} // 스타일
type={Camera.Constants.Type.front} // 카메라의 앞, 뒤 방향
cameraTextureHeight={textureDims.height} // 카메라 미리 보기 높이 값
cameraTextureWidth={textureDims.width} // 카메라 미리 보기 너비 값
resizeHeight={CAMERA_SIZE.height} // 출력 카메라 높이
resizeWidth={CAMERA_SIZE.width} // 출력 카메라 너비
resizeDepth={3} // 출력 텐서의 깊이(채널 수)값. (3 or 4)
autorender={true} // 뷰가 카메라 내용으로 자동업데이트 되는지 여부. (렌더링 발생시 직접적 제어를 원하면 false 값으로 설정할 것)
useCustomShadersToResize={false} // 커스텀 셰이더를 사용하여 출력 텐서에 맞는 더 작은 치수로 카메라 이미지의 크기를 조정할지 여부.
onReady={fn_onReadyTensorCamera} // 컴포넌트가 마운트되고 준비되면 이 콜백이 호출되고 다음 3가지 요소를 받습니다.
/>
</View>
)
}
export default App;
3. blazeface 모델 로드 및 예측
해당 부분에서는 화면이 렌더링 된 이후에 tensorflow.js를 사용하기 위해서 tf.ready() 함수를 수행시키면 해당 함수가 수행이 된 뒤 TensorCamera 출력이 되며 반환받은 'TensorImage'를 통해서 blazeface 모델 수행시켜서 사람을 예측합니다.
import { createRef, useEffect, useState } from 'react';
import { Platform, StyleSheet, View } from 'react-native';
import { Camera } from "expo-camera";
import { cameraWithTensors } from "@tensorflow/tfjs-react-native";
// Tensorflow
import * as tf from "@tensorflow/tfjs";
// Tensorflow Backend
import "@tensorflow/tfjs-backend-webgl";
// Tensorflow blazeface 모델을 가져온다.
const blazeface = require('@tensorflow-models/blazeface');
/**
* TensorflowCamera를 통해 얼굴을 측정하는 컴포넌트
* @returns
*/
const App: React.FC = () => {
// expo-camera를 통해서 TensorCamera를 구성한다.
const TensorCamera = cameraWithTensors(Camera);
// TensorCamera의 엘리먼트 정보를 가져온다.
const tensorCameraRef = createRef<any>();
// TensorCamera의 미리보기 운영체제 별 너비/높이 지정
const textureDims = Platform.OS === "ios" ? { height: 1920, width: 1080 } : { height: 1200, width: 1600 };
// 원하는 카메라의 사이즈를 지정한다.
const CAMERA_SIZE = { height: 480, width: 320 };
const [isTfReady, setIsTfReady] = useState<boolean>(false);
useEffect(() => {
(async () => {
await fn_requestPermisison();
await fn_tfReady();
})()
}, [])
/**
* 카메라 권한 요청 함수
*/
const fn_requestPermisison = async () => {
const { status } = (await Camera.requestCameraPermissionsAsync())
if (status !== 'granted') {
console.log("카메라의 권한 요청이 승인 되지 않았습니다.")
return;
}
};
/**
* Tensorflow Ready
*/
const fn_tfReady = async () => {
await tf.ready()
.then(() => {
setIsTfReady(true);
})
.catch((error) => {
console.log("(-) Tensorflow Ready Error ::: ", error);
return error;
});
}
/**
* blazeFace 모델을 통하여 얼굴 측정
* @param tensorImage
* @returns {boolean} 예측 시 true, 미 예측 시 false 반환
*/
const fn_estimateBlazeFace = async (tensorImage: any): Promise<boolean> => {
// 텐서 이미지가 존재하는지 체크
if (tensorImage) {
// [기능-1] blazeface 모델을 불러와서 측정을 수행한다
const blaze = await blazeface.load();
// [기능-2] blazeface 모델을 통하여 사람 얼굴을 측정
const predictions: any = await blaze.estimateFaces(tensorImage, false);
console.log(`[+] 측정 얼굴 수 [${predictions.length}]`);
// [CASE1-1] 얼굴을 예측 한 경우 true return
if (predictions.length > 0) {
return true;
}
// 텐서 이미지가 존재하지 않는 경우
else {
return false;
}
} else {
return false;
}
}
/**
* TensorCamera가 특정 시간 마다 루프를 돌면서 측정된 값을 반환 해줌.
* @param images : 카메라 이미지를 나타내는 텐서를 생성
* @param updatePreview : WebGL 렌더 버퍼를 카메라의 내용으로 업데이트하는 함수
* @param gl : 렌더링을 수행하는 데 사용되는 ExpoWebGl 컨텍스트
*/
const fn_onReadyTensorCamera = (images: any, updatePreview: any, gl: any) => {
const loop = async () => {
// TensorCamera에서 루프를 돌면서 나온 텐서 이미지
const nextImageTensor = images.next().value;
// blazeFace 모델을 통하여 얼굴 측정
await fn_estimateBlazeFace(nextImageTensor)
.then((isEstimate) => {
console.log("얼굴을 예측하였는가?", isEstimate);
})
.catch((error) => {
console.log(error);
})
// 2초간 반복적으로 루프를 반복한다.
setTimeout(() => {
requestAnimationFrame(loop);
}, 2000);
}
loop();
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
camera: {
position: "absolute",
width: CAMERA_SIZE.width,
height: CAMERA_SIZE.height,
},
});
return (
<View style={styles.container}>
{
isTfReady ?
<TensorCamera
ref={tensorCameraRef} // 엘리먼트 정보
style={styles.camera} // 스타일
type={Camera.Constants.Type.front} // 카메라의 앞, 뒤 방향
cameraTextureHeight={textureDims.height} // 카메라 미리 보기 높이 값
cameraTextureWidth={textureDims.width} // 카메라 미리 보기 너비 값
resizeHeight={CAMERA_SIZE.height} // 출력 카메라 높이
resizeWidth={CAMERA_SIZE.width} // 출력 카메라 너비
resizeDepth={3} // 출력 텐서의 깊이(채널 수)값. (3 or 4)
autorender={true} // 뷰가 카메라 내용으로 자동업데이트 되는지 여부. (렌더링 발생시 직접적 제어를 원하면 false 값으로 설정할 것)
useCustomShadersToResize={false} // 커스텀 셰이더를 사용하여 출력 텐서에 맞는 더 작은 치수로 카메라 이미지의 크기를 조정할지 여부.
onReady={fn_onReadyTensorCamera} // 컴포넌트가 마운트되고 준비되면 이 콜백이 호출되고 다음 3가지 요소를 받습니다.
/>
:
<></>
}
</View>
)
}
export default App;
4. 최종적으로 blazeface 모델을 통해 예측된 값으로 Canvas상에 그려줍니다.
해당 부분에서는 blazeface 모델을 통해서 반환 받은 값을 통해서 Canvas내에 얼굴을 그려주는 작업을 수행합니다.
import { createRef, useEffect, useState } from 'react';
import { Platform, StyleSheet, View } from 'react-native';
import { Camera } from "expo-camera";
import { cameraWithTensors } from "@tensorflow/tfjs-react-native";
import Canvas from 'react-native-canvas';
// Tensorflow
import * as tf from "@tensorflow/tfjs";
// Tensorflow Backend
import "@tensorflow/tfjs-backend-webgl";
// Tensorflow blazeface 모델을 가져온다.
const blazeface = require('@tensorflow-models/blazeface');
/**
* TensorflowCamera를 통해 얼굴을 측정하는 컴포넌트
* @returns
*/
const App: React.FC = () => {
// expo-camera를 통해서 TensorCamera를 구성한다.
const TensorCamera = cameraWithTensors(Camera);
// TensorCamera의 엘리먼트 정보를 가져온다.
const tensorCameraRef = createRef<any>();
const canvasRef = createRef<Canvas>();
// TensorCamera의 미리보기 운영체제 별 너비/높이 지정
const textureDims = Platform.OS === "ios" ? { height: 1920, width: 1080 } : { height: 1200, width: 1600 };
// 원하는 카메라의 사이즈를 지정한다.
const CAMERA_SIZE = { height: 480, width: 320 };
// 텐서 플로우가 준비가 되었는지 여부
const [isTfReady, setIsTfReady] = useState<boolean>(false);
useEffect(() => {
(async () => {
await fn_requestPermisison();
await fn_tfReady();
})()
}, [])
/**
* 카메라 권한 요청 함수
*/
const fn_requestPermisison = async () => {
const { status } = (await Camera.requestCameraPermissionsAsync())
if (status !== 'granted') {
console.log("카메라의 권한 요청이 승인 되지 않았습니다.")
return;
}
};
/**
* Tensorflow Ready
*/
const fn_tfReady = async () => {
await tf.ready()
.then(() => {
setIsTfReady(true);
})
.catch((error) => {
console.log("(-) Tensorflow Ready Error ::: ", error);
return error;
});
}
/**
* blazeFace 모델을 통하여 얼굴 측정
* @param tensorImage
* @returns {boolean} 예측 시 true, 미 예측 시 false 반환
*/
const fn_estimateBlazeFace = async (tensorImage: any): Promise<boolean> => {
// 텐서 이미지가 존재하는지 체크
if (tensorImage) {
// [기능-1] blazeface 모델을 불러와서 측정을 수행한다
const blaze = await blazeface.load();
// [기능-2] blazeface 모델을 통하여 사람 얼굴을 측정
const predictions: any = await blaze.estimateFaces(tensorImage, false);
console.log(`[+] 측정 얼굴 수 [${predictions.length}]`);
// [CASE1-1] 얼굴을 예측 한 경우 Canvas에 얼굴을 그려준다.
if (predictions.length > 0) {
await fn_drawRect(predictions); //조정된 위치에 대해서 캔버스에 그려주기
return true;
}
// [CASE1-2] 얼굴을 예측하지 못한 경우에 Canvas의 내용을 초기화 한다.
else {
if (canvasRef.current) canvasRef.current.getContext("2d").clearRect(0, 0, CAMERA_SIZE.width, CAMERA_SIZE.height);
return false;
}
}
// 텐서 이미지가 존재하지 않는 경우
else {
return false;
}
}
/**
* 조정된 위치에 대해서 캔버스에 그려주기
* @param predictions
*/
const fn_drawRect = async (predictions: any) => {
const canvas = canvasRef.current;
if (canvas) {
const ctx = canvas.getContext('2d');
for (let i = 0; i < predictions.length; i++) {
const start = predictions[i].topLeft;
const end = predictions[i].bottomRight;
const size = [end[0] - start[0], end[1] - start[1]];
// 그려 줄 캔버스 사이즈 지정
canvas.height = CAMERA_SIZE.height;
canvas.width = CAMERA_SIZE.width;
ctx.strokeStyle = "red"
ctx.lineWidth = 6;
// 화면상에 Rect를 그려준다.
ctx.strokeRect(start[0], start[1], size[0], size[1]);
}
}
}
/**
* TensorCamera가 특정 시간 마다 루프를 돌면서 측정된 값을 반환 해줌.
* @param images : 카메라 이미지를 나타내는 텐서를 생성
* @param updatePreview : WebGL 렌더 버퍼를 카메라의 내용으로 업데이트하는 함수
* @param gl : 렌더링을 수행하는 데 사용되는 ExpoWebGl 컨텍스트
*/
const fn_onReadyTensorCamera = (images: any, updatePreview: any, gl: any) => {
const loop = async () => {
// TensorCamera에서 루프를 돌면서 나온 텐서 이미지
const nextImageTensor = images.next().value;
// blazeFace 모델을 통하여 얼굴 측정
await fn_estimateBlazeFace(nextImageTensor)
.then((isEstimate) => {
console.log("얼굴을 예측하였는가?", isEstimate);
})
.catch((error) => {
console.log(error);
})
// 2초간 반복적으로 루프를 반복한다.
setTimeout(() => {
requestAnimationFrame(loop);
}, 2000);
}
loop();
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
camera: {
position: "absolute",
width: CAMERA_SIZE.width,
height: CAMERA_SIZE.height,
},
canvas: {
position: "absolute",
zIndex: 1000000,
}
});
return (
<View style={styles.container}>
{
isTfReady ?
<>
<TensorCamera
ref={tensorCameraRef} // 엘리먼트 정보
style={styles.camera} // 스타일
type={Camera.Constants.Type.front} // 카메라의 앞, 뒤 방향
cameraTextureHeight={textureDims.height} // 카메라 미리 보기 높이 값
cameraTextureWidth={textureDims.width} // 카메라 미리 보기 너비 값
resizeHeight={CAMERA_SIZE.height} // 출력 카메라 높이
resizeWidth={CAMERA_SIZE.width} // 출력 카메라 너비
resizeDepth={3} // 출력 텐서의 깊이(채널 수)값. (3 or 4)
autorender={true} // 뷰가 카메라 내용으로 자동업데이트 되는지 여부. (렌더링 발생시 직접적 제어를 원하면 false 값으로 설정할 것)
useCustomShadersToResize={false} // 커스텀 셰이더를 사용하여 출력 텐서에 맞는 더 작은 치수로 카메라 이미지의 크기를 조정할지 여부.
onReady={fn_onReadyTensorCamera} // 컴포넌트가 마운트되고 준비되면 이 콜백이 호출되고 다음 3가지 요소를 받습니다.
/>
{/* TensorCamera위에 그려줄 Canvas */}
<Canvas style={styles.canvas} ref={canvasRef} />
</>
:
<></>
}
</View>
)
}
export default App;
해당 글에서 작업한 내용은 하단의 Github으로 공유합니다.
오늘의 결론
Tensorflow.js를 통해서 얼굴을 예측하는 것에 대해서 공부해보았습니다.
이와 관련된 프로젝트에 활용하면 좋을 것으로 판단이 됩니다.
오늘도 감사합니다😀
반응형
'React & React Native > 라이브러리 활용' 카테고리의 다른 글
[RN] React Native 자이로센서를 활용한 자이로스코프, 디바이스 모션 이해하기: expo-sensors (0) | 2022.05.15 |
---|---|
[RN] React Native Tensorflow.js 메모리 관리 이해하기 (0) | 2022.04.10 |
[RN] React Native 앱 상태 관리 이해 및 구성 방법: AppState (0) | 2022.03.27 |
[RN] React Native expo-sqlite 이해 및 설정 방법 -1 : 환경 설정 및 DB, 테이블 생성, 기본 트랜잭션 (0) | 2022.03.01 |
[React] React-konva 이해하기-1 (구조편) (1) | 2022.01.29 |