이 글은 2021-1 캡스톤디자인프로젝트 기술블로그 제출용 글로, 전반적인 프로젝트의 내용과 프론트엔드 화면구성을 기술한 뒤 구현스킬 중 하나인,
카메라를 이용한 문서스캔 구현
에 대해 설명하겠습니다.
딥러닝 기반 언택트 학습 도우미 - 자동 채점 및 오답노트 생성
언택트시대에 선생님-학생 사이에서 숙제검사를 할 때 다음과 같은 프로세스로 진행됩니다.
이는 불필요한 과정이 중복되며 인력낭비를 발생시킵니다.
따라서 인적 자원을 절약하고 학원의 학생 관리가 용이할 수 있도록
수학 문제집 자동채점 및 오답노트 생성 앱 서비스(이하 VIVA
)를 제안하게 되었습니다.
React Native
를 이용하여 프론트엔드
를 개발하였습니다.
백엔드 연결
fetch
, await/async
이용Splash 페이지
ActivityIndicator
를 이용하여 로딩 애니메이션 구현AsyncStorage
를 이용한 로그인 여부 확인Main 페이지
문제집 검색 및 추가, 채점하기 등의 전반적인 모든 기능
react-native-raw-bottom-sheet
react-native-flash-message
회원가입 & 로그인 페이지
맞춤형 문제 추천 (=미니 모의고사) 페이지
마이페이지
FormData
로 전송하여 AWS S3에 업로드root가 되는 StackNavigator안에 Stack Navigator
와 Tab Navigator
을 쌓는 구조입니다.
이때 Tab Navigator
내부는 Stack Navigator
3가지로 이루어지며, 각 Stack Navigator
안에 Screen
이 존재합니다.
react-native-rectangle-scanner
를 이용하여 직사각형 문서를 인식하고 촬영하는 기능을 구현하였습니다.
npm install react-native-rectangle-scanner --save
직사각형을 detect 한 후 화면에 사각형을 표시해줘야 하기때문에 react-native-svg
를 추가로 설치해줍니다.
yarn add react-native-svg
Cocopods를 위한 단계
cd ios && pod install && cd ..
카메라 권한 설정
viva.xcworkspace 를 Xcode로 열어
Info.plist
파일 열어 다음 항목을 추가합니다.
OpenCV 연결
android/setting.gradle
include ':openCVLibrary310'
project(':openCVLibrary310').projectDir = new File(rootProject.projectDir,'../node_modules/react-native-rectangle-scanner/android/openCVLibrary310')
카메라 권한 설정
android/app/src/main/AndroidManifest.xml
<uses-permis
sion android:name="android.permission.CAMERA" />
카메라 접근 권한이 설정 된 모습
react-native-rectangle-scanner 깃헙에 올라와있는 샘플 코드를 기반으로,
필요에 맞게 수정하면서 구현하였습니다.
함수형 컴포넌트에서는 useLayoutEffect
를 사용해 헤더 커스터마이즈를 하였지만, 클래스 컴포넌트인 해당 코드는
componentDidMount
를 이용해 커스터마이즈합니다.
this.props.navigaion.setOptions
를 이용하여 옵션을 지정해줍니다.
이때 headerLeft
옵션에 뒤로 가기 버튼을 react-native-vector-icons
패키지를 이용해 커스텀해줍니다.
import Icon from 'react-native-vector-icons/Ionicons';
...
componentDidMount() {
if (this.state.didLoadInitialLayout && !this.state.isMultiTasking) {
this.turnOnCamera();
}
this.props.navigation.setOptions({
headerTitleAlign: 'left',
headerLeft: () => (
<TouchableOpacity
onPress={() => {
{
this.props.navigation.replace('Home');
}
}}>
<Icon
name="chevron-back-outline"
size={33}
style={{paddingLeft: 10}}
color="white"
/>
</TouchableOpacity>
),
});
}
feedbackState
라는 state를 추가하여 사진을 찍은 후에 true가 되도록 설정해줍니다.
아래 feedbackOverlay
화면은, 따라서 feedbackState
가 true일 때만 나타나는 화면입니다.
feedbackOverlay() {
if (this.state.feedbackState) {
return (
<>
<SafeAreaView style={[styles.overlay, {backgroundColor: 'white'}]}>
<View
style={{
height: hp(10),
justifyContent: 'flex-end',
alignItems: 'center',
paddingBottom: hp(3),
}}>
<Text style={{fontSize: wp(5), fontWeight: 'bold'}}>
📄 스캔 결과
</Text>
</View>
<View
style={{
justifyContent: 'center',
alignItems: 'center',
}}>
<ScrollView
style={{
height: hp(65),
// justifyContent: 'center',
// alignItems: 'center',
}}>
<AutoHeightImage
source={{
uri: this.state.currentImage,
}}
style={styles.feedbackImg}
width={wp(90)}
/>
</ScrollView>
</View>
{/*버튼*/}
<View
style={{
height: hp(15),
paddingTop: hp(7),
}}>
<View style={styles.btnContainer}>
<View style={styles.btnArea_l}>
<TouchableOpacity
style={styles.delbtnoutline}
onPress={() => {
{
this.feedback(1);
}
}}>
<Text>다시찍기</Text>
</TouchableOpacity>
</View>
<View style={styles.btnArea_r}>
<TouchableOpacity
style={styles.delbtn}
onPress={() => {
{
this.feedback(2);
}
}}>
<Text style={{color: 'white'}}>사용하기</Text>
</TouchableOpacity>
</View>
</View>
</View>
</SafeAreaView>
</>
);
}
}
사진을 찍었을 때 실행 되는 다음 onPictureProcessed
함수에서, croppedImage
에 직사각형으로 잘려진 이미지의 경로가 담겨있고,
currentImage
state에 그 경로값을 담아 현재 찍은 이미지의 경로를 저장해줍니다.
onPictureProcessed = ({croppedImage, initialImage}) => {
// this.props.onPictureProcessed(event);
this.setState({
takingPicture: false,
processingImage: false,
showScannerView: this.props.cameraIsOn || false,
feedbackState: true,
currentImage: croppedImage,
});
console.log('===initialImage===');
console.log(initialImage);
console.log('===croppedImage===');
console.log(croppedImage);
};
따라서 이미지의 uri this.state.currentImage
로 설정해주었을 때, 찍힌 사진의 미리보기를 제공할 수 있습니다.
<AutoHeightImage
source={{
uri: this.state.currentImage,
}}
style={styles.feedbackImg}
width={wp(90)}
/>
다시찍기와 사용하기 버튼을 눌렀을 경우, feedback
함수를 호출합니다.
<View style={styles.btnArea_l}>
<TouchableOpacity
style={styles.delbtnoutline}
onPress={() => {
{
this.feedback(1);
}
}}>
<Text>다시찍기</Text>
</TouchableOpacity>
</View>
<View style={styles.btnArea_r}>
<TouchableOpacity
style={styles.delbtn}
onPress={() => {
{
this.feedback(2);
}
}}>
<Text style={{color: 'white'}}>사용하기</Text>
</TouchableOpacity>
</View>
feedback
다시찍기의 경우, feedbackState: false
로 하여 스캔 결과 화면을 끄고 사용하기의 경우 스캔 결과 화면을 끈 후
preparedImgages
state 배열에 현재 찍은 이미지 파일의 경로를 append 해줍니다.
또한 한 번이라도 사용하기를 선택한 경우 isScanned: true
를 통해 카메라에 완료하기 버튼이 보이도록 해줍니다.
feedback = (option) => {
if (option == 1) {
//다시 찍기
this.setState({
feedbackState: false,
});
} else {
//사용하ㄱㅣ
this.setState({
feedbackState: false,
preparedImgages: [
...this.state.preparedImgages,
'file://' + this.state.currentImage,
],
isScanned: true,
});
console.log('====this.state.preparedImgages===');
console.log(this.state.preparedImgages);
}
};
위의 코드에서 this.state.preparedImgages
를 콘솔에 출력해보면
다음과 같이 찍힌 이미지의 경로가 제대로 담기는 것을 확인할 수 있습니다.
한 번이라도 사용하기를 누른 후에는 isScanned: true
에 의해, 카메라 스캔 화면에 완료 버튼이 나타납니다.
완료버튼을 누르면, postImages
함수를 호출합니다.
{this.state.isScanned && (
<TouchableOpacity
style={styles.completebtn}
onPress={() => {
{
this.postImages();
}
}}>
<Text style={{color: 'black', fontSize: wp(4.5)}}>완료</Text>
</TouchableOpacity>
)}
postImages
async, await, fetch를 이용해 데이터를 백엔로 보내는 과정입니다.
FormData는 서버에 파일을 보내기 위한 데이터 형식으로,
preparedImgages
state배열에 담긴 준비된 이미지 파일경로들을 Formdata로 만들고 이를 서버에 post하는 코드입니다.
이후 해당 이미지들은 백엔드에서 AWS S3로 업로드됩니다.
postImages = async () => {
const fd = new FormData();
// console.log('==postimage filedata==');
// console.log(filedata);
this.state.preparedImgages.forEach((image) =>
fd.append('mark', {
name: image,
uri: image,
type: 'image/jpeg',
}),
);
console.log('====fd===');
console.log(fd);
await axios
.post('http://192.168.0.3:3001' + '/api/paper-upload/', fd, {
headers: {
'content-type': 'multipart/form-data',
},
})
.then((res) => {
const response = res.data;
console.log(response.data.files);
console.log('The paper images is successfully uploaded');
// setUserPhoto(response.data.file.location);
// updateProfile(response.data.file.location);
})
.catch((err) => {
console.log('에러...');
console.error(err);
});
};
최종 결과 화면