일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 360도 뷰어
- 파노라마 뷰
- 3b52.1
- native
- 360도 이미지 뷰어
- privacyinfo.plist
- SwiftUI
- react-native-fast-image
- 뷰 생명주기
- launch screen
- 360도 이미지
- ssot
- ios
- react
- Android
- 명시적 정체성
- launchscreen
- React-Native
- requirenativecomponent
- 라이브러리 없이
- 리액트 네이티브
- panorama view
- React Native
- data driven construct
- 스켈레톤 통합
- 뷰 정체성
- 네이티브
- 리액트
- 구조적 정체성
- 앱 성능 개선
- Today
- Total
Neoself의 기술 블로그
Vision 프레임워크를 활용한 도서 표지 이미지 매칭 구현하기 본문
이번 포스트에서는 CoreML과 OCR을 활용한 도서 표지 인식 결과를 기반으로, 실제 도서를 매칭하는 시스템 구현 과정을 공유하고자 합니다. 특히, 초기 기획과는 다르게 변경된 설계 과정과 그 과정에서 마주친 문제들을 어떻게 해결했는지 자세히 다루어보겠습니다.
1. 초기 설계와 변경된 요구사항
1.1. 초기 설계
처음에는 다음과 같은 플로우를 계획했습니다:
- OCR 및 CoreML로 도서 표지에서 추출된 텍스트 제목/저자로 라벨링하여 도서 매칭 시스템에 전달
- 기존 구현된 searchOverallBooks 메서드로 도서 검색 후, 제목&저자 간 텍스트 유사도가 가장 높은 도서 반환
public func searchOverallBooks(from sourceBook: RawBook) -> Single<[BookItem]> {
Observable<Void>.just(())
.delay(.milliseconds(500), scheduler: ConcurrentDispatchQueueScheduler(qos: .background))
.flatMap { [weak self] _ -> Observable<[BookItem]> in
guard let self else {
return .error(BookMatchError.noMatchFound)
}
let titleSearch = naverAPI.searchBooks(query: sourceBook.title, limit: 10)
let authorSearch = naverAPI.searchBooks(query: sourceBook.author, limit: 10)
return Observable.zip(titleSearch, authorSearch)
.map { titleResults, authorResults in
var searchedResults = [BookItem]()
searchedResults.append(contentsOf: titleResults)
searchedResults.append(contentsOf: authorResults)
return searchedResults
}
}
.asSingle()
}
1.2. 변경된 요구사항
하지만 실제 구현 과정에서 몇 가지 중요한 제약사항이 발견되었습니다:
- 텍스트 라벨링 불가능: OCR 결과로 나온 텍스트들을 제목/저자로 명확히 구분할 수 없음
- 단순 텍스트 배열: 추출된 텍스트가 의미적 구분 없이 단어 배열로만 전달됨
// 예상되었던 OCR 검출로직 반환값
{
title: "북디자인 교과서"
author: "앤드루 해슬럼"
}
// 최종반환된 OCR 검출로직 반환값
{
textData:["북디자인", "앤드루", "교과서", "해슬럼", "안그라픽스"]
}
2. 문제 해결 접근
2.1. 새로운 가설 수립
문제 해결을 위해 다음과 같은 가설을 세웠습니다:
- 크기 기반 중요도: 도서 표지에서 가장 큰 텍스트는 제목이나 저자일 가능성이 높다
- 순차적 검색: 크기순으로 정렬된 텍스트들을 순차적으로 조합하여 검색하면 유의미한 결과를 얻을 수 있다
기존에는 제목과 저자 정보가 온전한 상태로 전달받았기 때문에, 단일 도서검색을 통해서도 유의미한 검색결과를 받아낼 수 있었지만, 반환된 반환값에서는 단어에 대한 명확한 구분기준이 없었기에 어디까지가 제목을 형성하는 단어인지 확실하지 않습니다.
하지만, 네이버 책검색 api의 경우, 제목 + 저자, 혹은 저자 + 제목 순서로 두 데이터를 검색 query에 같이 주입시켜도 두 조건을 모두 충족시키는 검색결과가 반환됩니다.
때문에, 어느 정도의 성능 리소스를 소요함으로서, 유의미한 검색결과를 최종반환할때 까지 검색 query에 단어들을 누적시키는 방향으로 전체 로직 방향을 재구성하게 되었습니다.
이를 바탕으로 CoreML 및 OCR 담당 팀원에게 크기 기반으로 텍스트 배열을 정렬하여 전달주는 것의 구현 가능성을 물어보았으며, 구현이 가능하다는 사실을 전달받을 수 있었습니다.
2.2. 새로운 검색 로직 구현
이러한 가설을 바탕으로 fetchSearchResults 메서드를 새롭게 구현했습니다:
private func fetchSearchResults(from textData: [String]) -> Single<[BookItem]> {
Single<[BookItem]>.create { single in
var searchResults = [BookItem]()
var previousResults = [BookItem]()
var currentIndex = 0
var currentQuery = ""
func processNextQuery() {
guard currentIndex < textData.count else {
single(.success(searchResults))
return
}
if currentQuery.isEmpty {
currentQuery = textData[currentIndex]
} else {
currentQuery = [currentQuery, textData[currentIndex]].joined(separator: " ")
}
return self.naverAPI.searchBooks(query: currentQuery, limit: 10)
.delay(.milliseconds(500), scheduler: ConcurrentDispatchQueueScheduler(qos: .background))
.subscribe(
onSuccess: { results in
if !results.isEmpty {
previousResults = results
}
if results.count <= 3 {
searchResults = previousResults
single(.success(searchResults))
} else if currentIndex == textData.count - 1 {
searchResults = results.isEmpty ? previousResults : results
single(.success(searchResults))
} else {
currentIndex += 1
processNextQuery()
}
},
onFailure: { error in
single(.failure(error))
}
)
.disposed(by: self.disposeBag)
}
processNextQuery()
return Disposables.create()
}
}
이 새로운 메서드의 주요 특징은 다음과 같습니다:
- 점진적 검색: 텍스트를 하나씩 추가하며 검색을 수행
- 결과 최적화: 검색 결과가 3개 이하가 되면 이전 결과를 사용
3. 매칭 정확도 향상을 위한 이미지 유사도 도입
3.1. 기존 방식의 한계
텍스트 유사도 기반 매칭의 한계점:
- OCR 불안정성: 도서 표지의 상세 설명까지 포함되어 노이즈 발생
- 매칭 변동성: 같은 책이라도 촬영 각도나 조명에 따라 다른 결과 도출
3.2. 이미지 유사도 기반 매칭
이러한 문제를 해결하기 위해 Vision 프레임워크를 활용해 2개의 이미지를 전달하면, 두 이미지 간 유사도를 0~1 사이 소수로 반환하는 ImageVisionStrategy를 새로 구현하였습니다.
public struct ImageVisionStrategy: SimilarityCalculatable {
public static func calculateSimilarity(_ image1: UIImage, _ image2: UIImage) -> Double {
let context = CIContext()
let processedImage1 = preprocessImage(image1, context)
let processedImage2 = preprocessImage(image2, context)
do {
let featurePrint1 = try extractFeaturePrint(from: processedImage1)
let featurePrint2 = try extractFeaturePrint(from: processedImage2)
var distance: Float = 0.0
try featurePrint1.computeDistance(&distance, to: featurePrint2)
return Double(max(0, min(1, 2.5 - distance * 2.5)))
} catch {
return -1.0
}
}
}
Vision 프레임워크는 Apple이 제공하는 컴퓨터 비전 프레임워크로, 이미지와 영상 분석을 위한 다양한 기능을 제공합니다. 저는 이 중에서도 특히 VNGenerateImageFeaturePrintRequest를 활용하여 이미지의 특징점을 추출하고 비교하는 기능을 사용했습니다.
3.3. ImageVisionStrategy 구조체 상세 분석
3.3.1. 전체 구조
먼저 ImageVisionStrategy 구조체의 전체적인 구조를 살펴보겠습니다:
public struct ImageVisionStrategy: SimilarityCalculatable {
public typealias T = UIImage
// 메인 유사도 계산 메서드
public static func calculateSimilarity(_ image1: UIImage, _ image2: UIImage) -> Double
// 이미지 특징점 추출 메서드
private static func extractFeaturePrint(from image: UIImage) throws -> VNFeaturePrintObservation
// 이미지 전처리 메서드
private static func preprocessImage(_ image: UIImage, _ context: CIContext) -> UIImage
}
3.3.2. 이미지 전처리 (preprocessImage)
이미지 전처리는 매칭의 정확도를 높이기 위한 첫 번째 단계입니다. 이 과정에서 UIImage를 CIImage로 변환하는데, 이는 Core Image 프레임워크의 강력한 이미지 처리 기능을 활용하기 위함입니다.
CIImage 변환의 이점
- 비파괴적 처리
- CIImage는 원본 이미지 데이터를 변경하지 않고 필터 체인을 구성
- 필터 적용 시점까지 실제 픽셀 처리를 지연시켜 메모리 효율성 확보
- 필터 체인 수정이나 롤백이 용이
- 고성능 처리
- GPU 가속을 통한 빠른 이미지 처리
- 메모리 사용량 최적화
- 실시간 이미지 처리에 적합
- 정밀한 필터링
- 16비트 및 32비트 부동소수점 색상 정밀도 지원
- 색상 보정 및 필터링 작업에서 더 정확한 결과 도출
- RAW 이미지 데이터 처리 가능
- Vision 프레임워크 호환성
- Vision 요청 처리 시 CIImage가 선호되는 입력 형식
- 추가적인 형식 변환 없이 바로 처리 가능
private static func preprocessImage(_ image: UIImage, _ context: CIContext) -> UIImage {
guard let ciImage = CIImage(image: image) else {
return image
}
// 1. 색상 보정 필터 생성
guard let filter = CIFilter(name: "CIColorControls") else {
return image
}
// 2. 필터 파라미터 설정
filter.setValue(ciImage, forKey: kCIInputImageKey)
filter.setValue(1.3, forKey: kCIInputContrastKey) // 대비 증가
filter.setValue(0.05, forKey: kCIInputBrightnessKey) // 밝기 미세 조정
// 3. 필터 적용 및 결과 변환
guard let outputImage = filter.outputImage,
let cgImage = context.createCGImage(outputImage, from: outputImage.extent) else {
return image
}
return UIImage(cgImage: cgImage)
}
- 이미지 변환: UIImage를 CIImage로 변환
- 대비 조정: 1.3배로 증가시켜 특징을 더 두드러지게 함
- 밝기 조정: 0.05만큼 밝기를 미세 조정하여 어두운 부분의 디테일 보존
3.3.3. 특징점 추출 (extractFeaturePrint)
Vision 프레임워크의 핵심 기능인 특징점 추출을 수행하는 메서드입니다:
private static func extractFeaturePrint(from image: UIImage) throws -> VNFeaturePrintObservation {
// 1. CIImage 변환
guard let ciImage = CIImage(image: image) else {
throw BookMatchError.networkError
}
// 2. Vision 요청 핸들러 생성
let requestHandler = VNImageRequestHandler(ciImage: ciImage, options: [:])
// 3. 특징점 추출 요청 생성
let request = VNGenerateImageFeaturePrintRequest()
do {
// 4. 요청 실행
try requestHandler.perform([request])
} catch {
throw error
}
// 5. 결과 추출 및 반환
guard let featurePrint = request.results?.first as? VNFeaturePrintObservation else {
throw BookMatchError.imageCalculationFailed("FeaturePrint 생성 실패")
}
return featurePrint
}
- 이미지 변환: Vision 프레임워크에서 처리 가능한 CIImage 형식으로 변환
- 요청 핸들러 생성: 이미지 처리를 위한 VNImageRequestHandler 인스턴스 생성
- 특징점 추출 요청: VNGenerateImageFeaturePrintRequest를 통한 특징점 추출
- 결과 검증: 추출된 특징점의 유효성 검사
3.3.4. 유사도 계산 (calculateSimilarity)
최종적으로 두 이미지 간의 유사도를 계산하는 메인 메서드입니다:
public static func calculateSimilarity(_ image1: UIImage, _ image2: UIImage) -> Double {
// 1. 컨텍스트 생성
let context = CIContext()
// 2. 이미지 전처리
let processedImage1 = preprocessImage(image1, context)
let processedImage2 = preprocessImage(image2, context)
do {
// 3. 특징점 추출
let featurePrint1 = try extractFeaturePrint(from: processedImage1)
let featurePrint2 = try extractFeaturePrint(from: processedImage2)
// 4. 거리 계산
var distance: Float = 0.0
try featurePrint1.computeDistance(&distance, to: featurePrint2)
// 5. 거리를 유사도 점수로 변환 (0~1 범위)
return Double(max(0, min(1, 2.5 - distance * 2.5)))
} catch {
return -1.0
}
}
- 전처리: 각 이미지에 대해 전처리 수행
- 특징점 추출: 각 이미지의 특징점 추출
- 거리 계산: 두 특징점 간의 거리 계산
- 점수 변환: 계산된 거리를 0~1 범위의 유사도 점수로 변환\\
4. 최종 구현 결과
전체적인 도서 매칭 프로세스는 다음과 같이 구현되었습니다:
- OCR/CoreML을 통한 텍스트 추출
- 크기 기반으로 정렬된 텍스트 배열 생성
- 순차적 도서 검색 수행
- 검색된 도서들의 썸네일 이미지 다운로드
- 이미지 유사도 기반 최종 매칭
이러한 구현을 통해 다음과 같은 이점을 얻을 수 있었습니다:
- 텍스트 인식의 불안정성 극복
- 이미지 기반의 안정적인 매칭
- 노이즈에 강한 매칭 시스템 구축
5. 테스트 및 검증
Vision 프레임워크 기반 이미지 매칭의 정확도를 검증하기 위해 아래와 같이 테스트 환경을 구축하였습니다.
func testImageSimilarity() {
let originalImage = UIImage(named: "book1")!
let similarImage = UIImage(named: "book1_different_angle")!
let differentImage = UIImage(named: "book2")!
// 동일 도서의 다른 각도 사진과의 유사도
let similarityScore1 = ImageVisionStrategy
.calculateSimilarity(originalImage, similarImage)
// 다른 도서와의 유사도
let similarityScore2 = ImageVisionStrategy
.calculateSimilarity(originalImage, differentImage)
// 동일 도서의 유사도가 더 높아야 함
XCTAssertGreater(similarityScore1, similarityScore2)
XCTAssertGreater(similarityScore1, 0.7) // 기준값 설정
}
'개발지식 정리 > Swift' 카테고리의 다른 글
BookMatchKit 리펙토링 과정-2/3 (Template Method 패턴) (2) | 2025.02.17 |
---|---|
BookMatchKit 리펙토링 과정-1/3 (Strategy 패턴, Adapter 패턴, Facade 패턴) (0) | 2025.02.17 |
ChatGPT와 네이버 책검색 api로 도서매칭 시스템 구현하기(고도화 과정 3/3) (0) | 2025.01.26 |
ChatGPT와 네이버 책검색 api로 도서매칭 시스템 구현하기(구현 과정 2/3) (1) | 2025.01.25 |
ChatGPT와 네이버 책검색 api로 도서매칭 시스템 구현하기(설계 과정 1/3) (0) | 2025.01.23 |