Swift ConcurrencyでOCR認識処理を並列化し最大6.7倍高速化する
OCRパイプラインの構造 iOSでONNX Runtimeを使ったOCRパイプラインは、大まかに以下の流れで動作します。 画像全体に対する文字領域検出(Detection) 検出された各領域に対する文字認識(Recognition) 読み順の推定と結合 この中で、検出は画像全体に対して1回だけ実行されます。一方、認識は検出された領域の数だけ繰り返されます。領域数が多い画像では、認識処理が全体の処理時間の大部分を占めることになります。 逐次処理の問題 認識処理を単純なforループで逐次実行すると、処理時間は領域数にほぼ比例します。 for det in detections { let rect = CGRect(x: max(0, det.box[0]), y: max(0, det.box[1]), width: max(1, det.box[2] - det.box[0]), height: max(1, det.box[3] - det.box[1])) if let cropped = image.cropping(to: rect) { _ = try recognizer.recognize(image: cropped) } } たとえば98領域が検出された画像の場合、1領域あたり約80msとすると合計で約8秒かかります。 withThrowingTaskGroupによる並列化 Swift ConcurrencyのwithThrowingTaskGroupを使うと、各領域の認識処理を並列に実行できます。 let results = try await withThrowingTaskGroup(of: (Int, String).self) { group in for i in 0..<detections.count { let box = detections[i].box let cropRect = CGRect( x: max(0, box[0]), y: max(0, box[1]), width: max(1, box[2] - box[0]), height: max(1, box[3] - box[1]) ) guard let cropped = image.cropping(to: cropRect) else { continue } group.addTask { let text = try recognizer.recognize(image: cropped) return (i, text) } } var texts: [(Int, String)] = [] for try await result in group { texts.append(result) } return texts } for (i, text) in results { recognized[i].text = text } 各タスクはインデックスとともに結果を返すため、完了順序に関係なく元の順序を保持できます。 ベンチマーク結果 iOSシミュレータ上で、逐次処理と並列処理の実行時間を比較しました。 古典籍(源氏物語、6642x4990px、21領域) 出典: 東京大学デジタルアーカイブ 源氏物語 方式 処理時間 逐次 4.55s 並列 3.24s 高速化率 1.4x 近代活字(NDLデジタルコレクション、6890x4706px、98領域) 出典: 国立国会図書館デジタルコレクション 方式 処理時間 逐次 7.59s 並列 1.13s 高速化率 6.7x 高速化率が異なる理由 21領域の場合と98領域の場合で高速化率に差が出ています。 ...
