-
CameraX api 5-1편 : OCR + 여권(MRZ + NFC)회사 생활/여권 NFC (CameraX + OCR + NFC) 2023. 11. 24. 20:11
CameraX 1편부터 4편까지 학습한 이유는 바로 회사에서 했었던 여권 MRZ 정보와 NFC로 정보 읽어오는 업무가 주어졌기 때문이다.
이번 프로젝트는 여러 오픈소스들과 CameraX API 학습한 내용을 조합해서 새로 만들어봤다.
우선 흐름을 간단히 살펴 보자면 :
1) CameraX API 카메라의 Analysis를 통해 실시간 Image 데이터를 가져온다.
2) Android MLKit을 활용해서 Image 데이터 내에 있는 Text를 OCR을 통해 가져온다.
3) 여권 MRZ라인을 통해 가져온 Text를 사용 가능한 정보로 변환한다.
4) 3번에서 얻어온 정보를 이용해서 여권 NFC를 통해 이미지와 여러 데이터를 가져온다.
앞에 Camera 1편 ~ 4편 내용은 1번 2번을 한 것이고 5편은 3번 4번 내용이다.
시작전에 간단히 여권 MRZ와 NFC를 알아보자면 아래와 같다.
여권 MRZ : 이미지에 나온 데이터를 얻을 수 있다.
여권 NFC : 링크
1. build.gradle
dependencies { ... 생략 ... implementation 'org.jmrtd:jmrtd:0.7.18' implementation 'net.sf.scuba:scuba-sc-android:0.0.18' implementation 'com.madgag.spongycastle:prov:1.58.0.0' //Secruity때문에 implementation 'edu.ucar:jj2000:5.2' implementation 'com.github.mhshams:jnbis:1.1.0' }
2. MRZ 판단 정규식
여권 MRZ인식에서 가장 중요한것은 MRZ의 판단 기준인것 같다.
val TD3_LINE1_REGEX = "P[A-Z<]([A-Z]{3})([A-Z<]*[A-Z])<<([A-Z<]*[A-Z])<*".toRegex() val TD3_LINE2_REGEX = "([A-Z0-9<]{9})[0-9]([A-Z]{3})(([0-9]{2})(0[0-9]|1[0-2])(0[1-9]|[1-2][0-9]|3[0-1]))[0-9]([MF<])(([0-9]{2})(0[0-9]|1[0-2])(0[1-9]|[1-2][0-9]|3[0-1]))[0-9]([A-Z0-9<]{14})[0-9]{2}".toRegex() val TD3_LINE1_REGEX_STR = "P[A-Z<]([A-Z]{3})([A-Z<]*[A-Z])<<([A-Z<]*[A-Z])<*" val TD3_LINE2_REGEX_STR = "([A-Z0-9<]{9})[0-9]([A-Z]{3})(([0-9]{2})(0[0-9]|1[0-2])(0[1-9]|[1-2][0-9]|3[0-1]))[0-9]([MF<])(([0-9]{2})(0[0-9]|1[0-2])(0[1-9]|[1-2][0-9]|3[0-1]))[0-9]([A-Z0-9<]{14})[0-9]{2}"
정규식이란 텍스트 데이터 중에서 원하는 조건(Pattern)과 일치하는 문자열을 찾아 내기 위해 사용하는 것이다.
3. OCR
OCR 부분은 이미 4편 있어서 수정사항만 추가해봤다.
private var lineTexts = arrayOfNulls<String>(2)
먼저 String Array가 추가 되었다.
private fun processTextBlock(result: Text) { var curText: String = "" val blocks: List<TextBlock> = result.textBlocks for (i in blocks.indices) { val lines = blocks[i].lines for (j in lines.indices) { val elements = lines[j].elements for (k in elements.indices) { curText += elements[k].text } lineTexts[0] = lineTexts[1] lineTexts[1] = curText parseMRZ() curText = "" } } }
여권 mrz가 2줄이기 때문에 화면속 text를 읽어오면서 2줄이 mrz text로 체워졌을때 NFC 프로세스로 넘어간다.
(lineText[0], lineText[1])
4. MRZ 라인 데이터로 변환
lineTexts는 여권 MRZ에서 2라인을 받기 위한 부분이다. 여러 Text들중 이 2부분이 lineTexts[0], lineText[1]에 들어오면 정보를 읽어오는 코드가 checkPassportMRZ에 추가 될것이다.
parseMRZ 메소드 전체 코드
더보기private fun parseMRZ() { if (lineTexts.size != 2 || lineTexts[1] == null || lineTexts[0] == null) { return } val mrzResult = MRZResult() if (!lineTexts[0]!!.matches(TD3_LINE1_REGEX) || !lineTexts[1]!!.matches(TD3_LINE2_REGEX)) { if (lineTexts[0]!!.matches(TD3_LINE2_REGEX) && lineTexts[1]!!.matches(TD3_LINE1_REGEX)) { lineTexts = arrayOf(lineTexts[1], lineTexts[0]) Log.d("parseTD3parseTD3", lineTexts[0] + " ---- " + lineTexts[1]) } else { return } } mrzResult.mrzText = """ ${lineTexts[0]} ${lineTexts[1]} """.trimIndent() mrzResult.isParsed = true mrzResult.isVerified = true mrzResult.docType = "passport" var pattern = Pattern.compile(TD3_LINE1_REGEX_STR) var matcher = pattern.matcher(lineTexts[0]) if (matcher.find()) { mrzResult.issuer = matcher.group(1) mrzResult.surname = matcher.group(2)?.replace('<', ' ') mrzResult.givenName = matcher.group(3)?.replace('<', ' ') Log.d("PassportMRZ", "" + mrzResult.issuer) Log.d("PassportMRZ", "" + mrzResult.surname) Log.d("PassportMRZ", "" + mrzResult.givenName) } else { Log.d("PassportMRZ", "PassportMRZ Error") return } //line2 pattern = Pattern.compile(TD3_LINE2_REGEX_STR) matcher = pattern.matcher(lineTexts[1]) if (matcher.find()) { mrzResult.docId = matcher.group(1).replace("<", "") if (!verifyString(matcher.group(1), lineTexts[1]!![9])) { mrzResult.isVerified = false //check digital of document number } mrzResult.nationality = matcher.group(2) mrzResult.dateOfBirth = matcher.group(4) + "-" + matcher.group(5) + "-" + matcher.group(6) if (!verifyString(matcher.group(3), lineTexts[1]!![19])) { mrzResult.isVerified = false //check digital of birth date } mrzResult.dateOfBirth = matcher.group(4) + matcher.group(5) + matcher.group(6) mrzResult.gender = matcher.group(7) mrzResult.dateOfExpiration = matcher.group(9) + "-" + matcher.group(10) + "-" + matcher.group(11) if (!verifyString(matcher.group(8), lineTexts[1]!![27])) { mrzResult.isVerified = false //check digital of expiration date } mrzResult.dateOfExpiration = matcher.group(9) + matcher.group(10) + matcher.group(11) if (!verifyString(matcher.group(12), lineTexts[1]!![42]) || !verifyString( lineTexts[1]!!.substring(0, 10) + lineTexts[1]!!.substring(13, 20) + lineTexts[1]!!.substring(21, 43), lineTexts[1]!![43] ) ) { mrzResult.isVerified = false //check digital of optional data and all } } else { return } Log.d("MRZ_Result", mrzResult.issuer!!) Log.d("MRZ_Result", mrzResult.surname!!) Log.d("MRZ_Result", mrzResult.givenName!!) Log.d("MRZ_Result", mrzResult.docId!!) Log.d("MRZ_Result", mrzResult.mrzText!!) Log.d("MRZ_Result", mrzResult.nationality!!) Log.d("MRZ_Result", mrzResult.dateOfBirth!!) Log.d("MRZ_Result", mrzResult.gender!!) Log.d("MRZ_Result", mrzResult.dateOfExpiration!!) val mrzInfo = buildTempMrz(mrzResult.docId, mrzResult.dateOfBirth, mrzResult.dateOfExpiration) if (isMrzValid(mrzInfo!!) && count < 1) { val intent = Intent(this, NfcScanActivity::class.java) intent.putExtra(MRZ_RESULT, mrzInfo) val application: MainApplication = applicationContext as MainApplication if(mBitmap.width > mBitmap.height){ mBitmap = rotateTheImage(90, mBitmap) } application.mBitmap = mBitmap startActivity(intent) finish() count++ } }
전체 코드가 너무 길어서 좀 쪼개서 보자면, 우선 맨 처음에 어떤 Text를 읽어오든 한줄만 읽어오면 lineTexts[1]은 비어있을 것이기 때문에 이부분은 생략해준다.
if (lineTexts.size != 2 || lineTexts[1] == null || lineTexts[0] == null) { return }
mrz라인을 읽어올때 2번째 라인과 1번째 라인이 반대로 읽었을 경우 바꿔주는 코드이다. 그러나 거의 이런 경우는 없다.
val mrzResult = MRZResult() if (!lineTexts[0]!!.matches(TD3_LINE1_REGEX) || !lineTexts[1]!!.matches(TD3_LINE2_REGEX)) { if (lineTexts[0]!!.matches(TD3_LINE2_REGEX) && lineTexts[1]!!.matches(TD3_LINE1_REGEX)) { lineTexts = arrayOf(lineTexts[1], lineTexts[0]) Log.d("parseTD3parseTD3", lineTexts[0] + " ---- " + lineTexts[1]) } else { return } }
mrz 첫번쨰 줄에서 국적과 이름을 차례로 가져온다.
var pattern = Pattern.compile(TD3_LINE1_REGEX_STR) var matcher = pattern.matcher(lineTexts[0]) if (matcher.find()) { mrzResult.issuer = matcher.group(1) mrzResult.surname = matcher.group(2)?.replace('<', ' ') mrzResult.givenName = matcher.group(3)?.replace('<', ' ') } else { Log.d("PassportMRZ", "PassportMRZ Error") return }
mrz 두번째 줄에서는 여권번호나 만료일자 성별 등등을 가져온다.
pattern = Pattern.compile(TD3_LINE2_REGEX_STR) matcher = pattern.matcher(lineTexts[1]) if (matcher.find()) { mrzResult.docId = matcher.group(1).replace("<", "") if (!verifyString(matcher.group(1), lineTexts[1]!![9])) { mrzResult.isVerified = false //check digital of document number } mrzResult.nationality = matcher.group(2) mrzResult.dateOfBirth = matcher.group(4) + "-" + matcher.group(5) + "-" + matcher.group(6) if (!verifyString(matcher.group(3), lineTexts[1]!![19])) { mrzResult.isVerified = false //check digital of birth date } mrzResult.dateOfBirth = matcher.group(4) + matcher.group(5) + matcher.group(6) mrzResult.gender = matcher.group(7) mrzResult.dateOfExpiration = matcher.group(9) + "-" + matcher.group(10) + "-" + matcher.group(11) if (!verifyString(matcher.group(8), lineTexts[1]!![27])) { mrzResult.isVerified = false //check digital of expiration date } mrzResult.dateOfExpiration = matcher.group(9) + matcher.group(10) + matcher.group(11) if (!verifyString(matcher.group(12), lineTexts[1]!![42]) || !verifyString( lineTexts[1]!!.substring(0, 10) + lineTexts[1]!!.substring( 13, 20 ) + lineTexts[1]!!.substring(21, 43), lineTexts[1]!![43] ) ) { mrzResult.isVerified = false //check digital of optional data and all } } else { return }
가져온 데이터중 여권번호, 생년월인, 여권만료일자를 이용해 MRZInfo 객체를 만들고 NFC를 활용할 Activity에 넘겨준다.
MRZInfo는 build.gradle에 implements를 했던 내용들에 포함되어 있다. 그리고 MRZ로 얻을 수 있는 정보들을 가지고 있다.
val mrzInfo = buildTempMrz(mrzResult.docId, mrzResult.dateOfBirth, mrzResult.dateOfExpiration) if (isMrzValid(mrzInfo!!) && count < 1) { val intent = Intent(this, NfcScanActivity::class.java) intent.putExtra(MRZ_RESULT, mrzInfo) val application: MainApplication = applicationContext as MainApplication if (mBitmap.width > mBitmap.height) { mBitmap = rotateTheImage(90, mBitmap) } application.mBitmap = mBitmap startActivity(intent) finish() count++ }
5. 나머지 코드들
fun buildTempMrz(documentNumber: String?, dateOfBirth: String?, expiryDate: String?): MRZInfo? { var mrzInfo: MRZInfo? = null try { mrzInfo = MRZInfo("P", "NNN", "", "", documentNumber, "NNN", dateOfBirth, Gender.UNSPECIFIED, expiryDate, "") } catch (e: java.lang.Exception) { Log.d(TAG, "MRZInfo error : " + e.localizedMessage) } return mrzInfo } private fun verifyString(s: String, c: Char): Boolean { return if (c < '0' || c > '9') { false } else compute(s) == c.toString().toInt() } private fun compute(source: String): Int { val s = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" val w = intArrayOf(7, 3, 1) var c = 0 for (i in 0 until source.length) { if (source[i] == '<') continue c += s.indexOf(source[i]) * w[i % 3] } c %= 10 return c }
NFC 내용까지 여기에 적으면 너무 길어져서 자세한 내용은 다음편에 해당 내용을 다룰 것이다.
다음편 :
https://from-android-to-server.tistory.com/87
Github : https://github.com/tvroom88/AIO_Android_Kotlin_Support_Material/tree/main/CameraX/CameraX_5_OCR_NFC
'회사 생활 > 여권 NFC (CameraX + OCR + NFC)' 카테고리의 다른 글
CameraX api 5-2편 : OCR + 여권(MRZ + NFC) (1) 2023.11.25 CameraX api 번외편 : guideline에 따라 이미지 자르기 (1) 2023.11.20 CameraX api 4-2편 : Analysis + OCR + TextGraphic (1) 2023.11.18 CameraX api 4편 : Anlysis와 text 분석 라이브러리(OCR) (0) 2023.11.14 CameraX api 3편 : takePicture 메소드 (0) 2023.11.13