iyOmSd/Title: Swift

[Swift] Vision OCR 인식

냄수 2025. 6. 25. 21:34
반응형

Vision Framework를 사용해서 이미지속의 텍스트를 인식할 수 있는 OCR 기능을 구현해보려합니다

 

OCR 이란?

광학 문자 인식 Optical Character Recognition의 약자로 이미지나 스캔 문서에 있는 텍스트를 기계가 읽을 수 있는 텍스트로 변환하는 기술입니다.

 

애플에서 제공되는 텍스트 인식기능은

2가지 방법으로 이용합니다

fast path방식과 accurate path방식이 있습니다. 

 

Fast

프레임워크의 문자 감지 기능을 사용해서 개별문자를 찾은다음, 작은 머신 러닝 모델을 사용하여 개별 문자와 단어를 인식

이 접근방식은 기존 OCR과 유사함

 

Accurate (default)

신경망을 이용해서 문자열과 줄로 이루어진 텍스트를 찾은다음 추가 분석을 수행하여 개별 단어와 문장을 찾음

이 접근방식은 사람이 텍스트를 읽는 방식과 훨씬 더 일치함

 

VNRequestTextRecognitionLevel타입으로 제공되고

VNRecognizeTextRequest의 recognitionLevel옵션을 변경하면 적용됩니다

 

let textRequest = VNRecognizeTextRequest()
textRequest.recognitionLevel = .accurate

 

 

구현

단순 OCR인식 코드구현은 간단합니다

사용하기 편리하게 제공된거같네요

guard let image = uiImage.cgImage else {
    print("cgImage nil!!")
    return
}

let textRequest = VNRecognizeTextRequest()
textRequest.revision = VNRecognizeTextRequestRevision3
textRequest.recognitionLanguages = ["ko-KR", "en-US"]
textRequest.usesLanguageCorrection = false
textRequest.minimumTextHeight = 0.1
textRequest.recognitionLevel = .accurate

let handler = VNImageRequestHandler(cgImage: image)
do {
    // 요청실행
    try handler.perform([textRequest])
    
    // 요청값 반환
    let text = textRequest.results?.compactMap { $0.topCandidates(1).first?.string }.joined(separator: "\n") ?? ""
    print(text)
}

 

Request 옵션을 하나씩알아볼게요

 

revision - 엔진 버전 선택(최신이 좋아요 iOS지원 버전에맞게 선택하세요!)

 

recognitionLanguages - 인식할 언어

textRequest.supportedRecognitionLanguages()

위의코드를 실행해서 인식가능한 언어목록을 확인할 수 있어요

인식할 언어 ko-KR를 안넣어주면 한국어가 있어도 한국어로 인식하지않아요

 

usesLanguageCorrection - 인식 프로세스 중에 언어보정을 할건지 옵션이에요

비활성화하면 원시 인식 결과값이 반환되서 성능은 좋지만 정확도가 떨어져요

 

minimumTextHeight - 이미지 높이에 상대적인 크기의 텍스트를 인식하는 옵션이에요

예를들어 이미지높이의 절반인 텍스트로 인식을 제한하려면 0.5를 넣어요

크게크게 보기때문에 메모리 사용량이 줄고, 인식속도가 빨라지지만 작은크기 텍스트는 무시합니다.

기본값은 0.03125 라고하네요

(테스트할때 0.1~0.9 다 비슷하게나와서 체감을 못하겠어요 🤔)

 

recognitionLevel - 위에서 말한 인식요청 우선순위를 정하는 옵션입니다.

 

VNImageRequestHandler의 perform()

요청을 수행하는 함수입니다.

이때 인식이 시작됩니다

try handler.perform([textRequest])

 

 

VNRecognizeTextRequest.results 

perform을 했을때 VNRecognizedTextObservation타입의 결과값 배열이 반환되요

    let text = textRequest.results?.compactMap { $0.topCandidates(1).first?.string }.joined(separator: "\n") ?? ""

$0.topCandidates(1) - 인식결과값이 여러개인데 그중 우선순위 최상위 1개만 받겠다 라는 옵션이에요

인식한 문자 하나하나의 결과값을 joined시켜서 하나의 문장으로 만들어주는 코드에요

 

실험

인식시킬 테스트 이미지를 만들어봤어요

각 글씨크기별로 인식차이와 영어 한글 숫자 차이를 알아보려합니다!

 

결과를 극단적으로 비교하기위해서

우선 처음테스트는 정확도우선의 옵션을 설정했어요

let textRequest = VNRecognizeTextRequest()
textRequest.revision = VNRecognizeTextRequestRevision3
textRequest.recognitionLanguages = ["ko-KR", "en-US"]
textRequest.usesLanguageCorrection = true
textRequest.recognitionLevel = .accurate

 

출력값은 아래와같아요

12 point 포인트
14 point 포인트
16 point 포인트
20 point 포인트
24 point 포인트
32 point 포인트
36 point 포인트
40 point 포인트
48 point 포인트
0123456789 0123456789
01234567890123456789
0123456789 0123456789
0123456790123456789
01234567890123456789
01234567890123456789
01234567890123456789
01234567890123456789
01234567890123456789
64 point 포인트
01234567890123456789
1 장, 0.800754084 seconds

정확하게 잘 인식하네요

하지만 이때 고려해야할게 하나 있죠 바로 시간입니다

1장을 인식하는데 0.8초라 여러장의 사진을 인식하려한다면 고려해야할 부분이 많아지겠죠?


다음으론

빠름우선의 옵션을 설정해볼게요 

let textRequest = VNRecognizeTextRequest()
textRequest.revision = VNRecognizeTextRequestRevision3
textRequest.recognitionLanguages = ["ko-KR", "en-US"]
textRequest.usesLanguageCorrection = false
//        textRequest.minimumTextHeight = 0.1  << fast일때 설정하면 아무결과를 받지못합니다
textRequest.recognitionLevel = .fast

 

출력값은 아래와같아요

12 point &oI_
01234587890123456789
14 point EOI
01234567890123456789
16 point EOIE
01234567890123456789
20 point £oI E
01234567890123456789
24 point £oI E
01234567890123456789
32 point &OI E
01234567890123456789
36 point &oIE
01234567890123456789
40 point £OIE
01234567890123456789
48 point lOI E
01234567890123456789
64 point lOI E
01234567890123456789
1 장, 0.077625583 seconds

 12point 테스트케이스인 1번째줄은 6과 8을 정확하게 인식하지못햇네요

나머지 숫자와 영어는 잘인식되늰데 한글을 인식하지못하네요

그리고 위코드에서 보이듯이 minimumTextHeight를 fast옵션과 함께주면 텍스트인식을 못해서 아무결과값을 받지못해요

시간도보면 0.07초로 10배차이가 나는걸 볼 수있어요

 

시간과 성능의 트레이드오프인 옵션들이라 테스트하면서 잘 선택하면 좋을 것 같아요!


마지막으로 이젠 여러장을 인식할때 async로 동시에 돌리고싶을때를 가정하고

코드를 한번 바꿔볼게요

 

텍스트인식 구현부는 아래와같구요

private func excuteRequest(uiImage: UIImage) async -> [VNRecognizedTextObservation] {
    guard let image = uiImage.cgImage else {
        print("cgImage nil!!")
        return []
    }
    let textRequest = VNRecognizeTextRequest()
    textRequest.revision = VNRecognizeTextRequestRevision3
    textRequest.recognitionLanguages = ["ko-KR", "en-US"]
    textRequest.usesLanguageCorrection = false
//        textRequest.minimumTextHeight = 0.1
    textRequest.recognitionLevel = .fast
    
    let handler = VNImageRequestHandler(cgImage: image)
    
    return await withUnsafeContinuation { continuation in
        do {
            try handler.perform([textRequest])
            let observations = textRequest.results ?? []
            continuation.resume(returning: observations)
        } catch {
            print(error)
            continuation.resume(returning: [])
        }
    }
}

 

상위에서 이미지를 넣어주는 코드는 아래와같습니다.

func recognizeText() async {
    let images: [UIImage] = [
        .폰트테스트png,
        .폰트테스트png,
    ]
    
    let time = await clock.measure {
        // 이미지 총갯수를 4개의 이미지그룹으로 분리
        let imageGroups = images.splitFourStride()
        
        let recognizationString = await withTaskGroup(of: String.self) { group in
            imageGroups.forEach { imageGroup in
                group.addTask {
                    await self.excute(imageGroup: imageGroup)
                }
            }
            var result: String = ""
            for await value in group {
                result += value
            }
            return result
        }
        
        // 종합된 결과 텍스트
        print(recognizationString)
    }
    
    // 인식한 장수, 인식까지 걸린시간
    print(images.count, "장", time)
}

// 각 그룹별 인식결과 반환
private func excute(imageGroup: [UIImage]) async -> String {
    var totalString = ""
    for (i, image) in imageGroup.enumerated() {
        let results = await self.excuteRequest(uiImage: image)
        let text = results.compactMap { $0.topCandidates(1).first?.string }.joined(separator: "\n")
        totalString += text
    }
    return totalString
}

 

10개의 이미지를 4개의 그룹으로 실행시킨 결과 아래와같이 동시에 잘 진행되는걸 확인할 수 있어요

 

 

비교를위해 그룹을 나누지않는다면 아래와같은 그림처럼 동시에 진행하진 않는걸 볼 수 있어요

 

 

 

반응형