CHILDRAW IOS App

신경망 학습 그림 그리기를 이용한 유아 영어 교육 애플리케이션

개요

IT를 접목한 유아 영어 교육인 에듀테크, 재미와 반복학습의 특징을 가지고 있는 그림 그리기를 접목시켜, 아이들의 영어 발음을 인식하는 음성인식(DNN), 실시간으로 서버와 통신해 아이들의 그림의 특징을 추출할 수 있는 이미지 인식(CNN)으로 구성된 영어 교육 애플리케이션.

프로젝트 기간

2017년 12월 1일 ~ 현재 개발중 (5월 초 목표)

프로젝트 등급

메인 기능 구현 완료
부가기능 (마이페이지) 구현 중

GitHub Repository

https://github.com/godpp/chilDraw_IOS

워크플로우

개발설명

스플래쉬 & 로그인 화면

애플리케이션의 시작인 스플래쉬와 로그인 화면이다. 이전 프로젝트들과 마찬가지로 CenterConstraintY를 Outlet으로 선언해 키보드가 올라왔을 때 자연스럽게 StackView가 상승하도록 구현하였다.

아래의 코드는 Alamofire을 이용해 AWS EC2 서버와 통신하는 메소드를 구현한 것이다. 로그인에 성공 시 서버로 부터 msg = “1”을 받게 되는데,success case문 안에 첫 번째 if문을 보게 되면 token을 서버로 부터 받아 UserDefault에 저장하는 것을 볼 수 있다. 이는 후에 user의 고유 token을 이용한 인증방식을 쓰기 위함이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Alamofire.request(URL, method: .post, parameters: body, encoding: JSONEncoding.default, headers: nil).responseObject{
(response:DataResponse<LoginVO>) in
switch response.result {
case .success:
guard let Message = response.result.value else{
self.view.networkFailed()
return
}
if Message.msg == "1" {
if let token = Message.data{
ud.setValue(token, forKey: "token")
ud.synchronize()
}
self.view.networkResult(resultData: "", code: "1")
}
else {
self.view.networkResult(resultData: "error", code: "2")
}


case .failure(let err):
print(err)
self.view.networkFailed()
}
}

회원가입 화면

회원가입 뷰는 총 두 개의 뷰로 이루어져 있다. 첫 번째 뷰에선 유저네임과 이메일의 중복확인을 하게 되는데, 아래와 같이 addTarget 메소드를 이용하여 TextField가 editingChanged 상태일 때 실시간으로 중복인지 아닌지 검사할 수 있게 구현하였다.

1
2
3
4
5
6
7
8
9
func initAddTarget(){
usernameTxt.addTarget(self, action: #selector(isValid), for: .editingChanged)
usernameTxt.addTarget(self, action: #selector(duplicateCheck), for: .editingChanged)
emailTxt.addTarget(self, action: #selector(isValid), for: .editingChanged)
emailTxt.addTarget(self, action: #selector(duplicateCheck), for: .editingChanged)
pwdTxt.addTarget(self, action: #selector(isValid), for: .editingChanged)
confirmpwdTxt.addTarget(self, action: #selector(isValid), for: .editingChanged)
confirmpwdTxt.addTarget(self, action: #selector(confirmCheck), for: .editingChanged)
}

두번째 뷰는 현재 회원가입 요소를 추가할 것인가에 대한 고민을 하고있다.

메인화면

로그인 후 보게되는 메인화면이다. 아이들이 써야 하는 특성상 전체적인 뷰구조를 어렵게 구성하지 않았고, 메인화면에서 마이페이지, 도움말, 그림 그리기 화면으로 이동할 수 있다. 단어 카테고리를 어떻게 직관적으로 보여줄까에 대한 생각을 하였고, CollectionView를 이용해 카테고리를 옆으로 넘겨 볼 수 있게 구현하였다. 카테고리를 선택하고 START 버튼을 클릭하면 그림 그리기 화면으로 이동할 수 있다.

카테고리 선택시 선택된 카테고리 정보가 필요했고, 이는 cell을 sender로 받아 0~5까지 숫자를 부여하여 해결하였다. 아래는 이에 대한 함수이다.

1
2
3
4
5
6
// 카테고리 클릭시 해당 값 호출
func categoryBtnPressed(cell: categoryCell) {
let indexPath = self.categoryView.indexPath(for: cell)
choiceCategoryNum = gino(indexPath?.row)
print(choiceCategoryNum)
}

그림 그리기 화면

영어 교육 애플리케이션의 핵심인 그림 그리기 화면이다. 그림 그리는 부분만 따로 뷰를 만들어 클래스를 선언했다. 또한 구글의 인셉션V3모델에 학습 시키기 위해선 실시간으로 사용자가 그림을 그릴 때마다 x좌표와 y좌표값이 필요했는데, 터치의 시작과 때는 순간을 1번으로 보고 서버로 좌표값을 각각 배열에 담아 보내주었다. 아래의 코드는 터치 시작,움직임,종료의 세가지 메소드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard !isDrawing else { return }
isDrawing = true
guard let touch = touches.first else { return }
let currentPoint = touch.location(in: self)
lastPoint = currentPoint
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard isDrawing else { return }
guard let touch = touches.first else { return }
let currentPoint = touch.location(in: self)
let stroke = Stroke(startPoint: lastPoint, endPoint: currentPoint, color: strokeColor)
strokes.append(stroke)
lastPoint = currentPoint
drawX.append(Int(currentPoint.x))
drawY.append(Int(self.frame.size.height) - Int(currentPoint.y))

setNeedsDisplay()
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard isDrawing else { return }
isDrawing = false
guard let touch = touches.first else { return }
let currentPoint = touch.location(in: self)
let stroke = Stroke(startPoint: lastPoint, endPoint: currentPoint, color: strokeColor)
strokes.append(stroke)
lastPoint = nil
drawingArray.append(drawX)
drawingArray.append(drawY)
let model = MainModel(self)
model.drawModel(word: "grape", string: "\(drawingArray)")
print("\(drawingArray)")
drawingArray.removeAll()
print(data)
if data == 1{
print("정답")
}
else{
print("오답")
}

setNeedsDisplay()

}

음성인식을 위한 녹음 기능은 Xcode의 내장 녹음 기능인 AVFoundation을 import시켜 사용하였다. 아래와 같은 코드로 녹음파일을 .wav로 저장시켜 서버로 전송시켰다. (유아가 쓰는 특성상 시간제한을 4초로 두었다.)

1
2
3
4
5
6
7
8
9
let audioFilename = getDocumentsDirectory().appendingPathComponent("voice.wav")

audioRecorder = try AVAudioRecorder(url: audioFilename, settings: settings)
audioRecorder.delegate = self
audioRecorder.isMeteringEnabled = true
audioRecorder.record()
meterTimer = Timer.scheduledTimer(timeInterval: 0.1, target:self, selector:#selector(self.updateAudioMeter(timer:)), userInfo:nil, repeats:true)
print(audioFilename)
audioURL = try audioRecorder?.url

아래는 녹음파일을 Alamofire의 multipart에 담아 서버로 전송하는 메소드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func recordModel(voice: Data?) {
let URL : String = "\(baseURL)/test"

Alamofire.upload(
multipartFormData: { multipartFormData in
multipartFormData.append(voice!, withName: "voice", fileName: "voice.wav", mimeType: "audio/wav")
},
to: URL,
encodingCompletion: { encodingResult in
switch encodingResult {
case .success(let upload, _, _):
upload.responseData { response in
debugPrint(response)
}
case .failure(let encodingError):
print(encodingError)
}
}
)
}