ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Visionによるドキュメントデータの抽出
Visionを使用して、アプリ内で専門的な画像認識・分析を行い、ドキュメントから情報を抽出したり、多言語のテキストを認識したり、バーコードを識別したりする方法を紹介します。本セッションでは、テキスト認識とバーコード検出の最新のアップデートについて紹介し、これらのツールをCore MLと組み合わせて、画像やライブカメラを通してアプリが環境をより理解できるようにする方法について説明します。Visionによって提供されている機能をさらに理解するには、WWDC21の「Visionによる人物、顔、ポーズの検出」とWWDC20の「Computer Vision APIの探求」をご確認ください。
リソース
関連ビデオ
WWDC21
WWDC20
WWDC19
-
ダウンロード
こんにちは 私はフランク・ドプケ Visionチームの エンジニアです WWCCにようこそ!
Visionは年々 進化を重ね 画像解析に 重点を置いてきました Visionの よりご理解いただくために 応用分野ごとに 見ていきましょう まず スポーツ オブジェクトの追跡や 人間のポーズの分析など スポーツAppに応用できる 技術は多々あります
次に アクセシビリティ 文字読み取りや 画像認識 画像分類で 目の不自由な方に
貢献します なお Visionは 多くの顔・生体認証技術を App向けに公開しており 「Visionによる人物、顔、ポーズの検出」 にて詳しい説明がございます
健康分野でも バーコード読み取りや文字認識 人間のポーズの分析で 有益な医療ソフトの 基盤を支えます
カメラの自動加工においては ポートレートモードなどで 顔の検出とセグメンテーションを 使っています
お次は セキュリティ 顔や人間の検出は 防犯カメラの 動作センサーで使います
最後に ドキュメント 本セッションではこの分野に 焦点を当てています
Visionは 文書分析関連の さまざまな要求に 対応可能です バーコード読み取りや OCRの略称で知られる文字認識 輪郭検出 長方形検出 そして今年新開発の ドキュメント検出技術
課題は以下の通りです まずバーコード読み取りについて 次に文字認識 最後に文書検出について説明します
バーコード読み取りですが 今年はバーコード読み取り技術に 新たな修正を加えました
VNDetectBarcodesRequestRevision2を用いると バーコードの新らしい形式である Codabarや 拡張版・短縮を含む GS1Databar MicroPDF MicroQRに対応可で 特にMicro QRは 空きスペースが小さい場合 例えば包装面が小さい商品で 重宝されます
指定された 注目画像領域(ROI)の バウンディングボックスを 記録する過程を 今回の修正で Visionの他製品と 統一することができました
この修正を 詳しく図解しましょう ここにQRコード載った 文書がありますね ROI 注目画像領域を 指定しなかった場合 境界ボックスの位置は 画像全体を基準に 算出されます 今度はROIを 指定します 画像の中心部だけに 集中するよう 指示をします Revision2 では 境界ボックスが 他のVision仕様と同じよう ROI基準で算出されます
ただ Revision 1下のコマンドは 依然として画像全体を 基準としていますが それを無理に改変して 帳消しにすることは ありません また 念押しですが 最新のSDKを 使ってのAppビルドで リビジョンを指定しなかった場合 最新のリビジョンを適用します ただ Revision1を ご指定の場合や 最新のSDKへの 更新がない場合は 既存のRevision1の仕様が 維持されます
Visionの バーコード読み取りが 優秀な理由を ご紹介します Visionは 1D・2D双方に対応可ですが
驚くことに 一つの画像内で 複数のコード 複数の種類 形式:シンボロジーを 一度に読み取るということです
つまり複数個のコードの読み取りに 何度も スキャンする必要がないのです ハンドヘルドスキャナと比べ 大きな利点となります もちろん 複数のシンボロジーを 読み取る場合 処理時間は増えますので 用途に応じて シンボロジーを絞ることが 必要です
バーコード読み取りにおいて サポートシンボロジーの拡張により Visionは医療分野で 特に活躍します iPhoneがあれば スキャナ不要で 複数のコードを 一度に読み取れ インターネットに接続すれば 情報を入手できます また iPhoneは 暗所撮影能力が優れていて 暗い場所でも レーザーを照射したり 患者の休息を 邪魔せず 情報を 読み取ることができます
次は Visionの 読み取り過程についてです
一次元のバーコードは 通常 線状にスキャンします 一つのコードながら 何度も検出されて しまうのです 重複を排除するのは簡単で バーコードをデコードした データが格納される ペイロードを参照します
一方 2Dのコードは単一の ユニットとしてスキャンされます 一つのコードに対して 境界ボックスが一つだけ得られます 2Dコードの典型が QRコードです
それぞれのコードは 別々の結果として報告されます ただ 先ほど述べた通り 1Dコードは同一の内容を 幾度にわたり読み取ることも ありえます ペイロードはバーコードのコンテンツ つまり、この機械が読めるコードが 含まれるデータです QRコードのペイロードの場合は URLをデコードするのに Data Detecterを使用します
もう少し 掘り下げましょう
XcodeのPlaygroudです 様々なバーコードが 画面に ちりばめられていまいます NDetectBarcodesRequest を使い Revision は 2 に設定 形式 symbologies はというと codabar です こう操作すると コーダバーが赤く反映されるのが 分かりますね
QRコードの場合 どうでしょう?
これも このリクエストを入力すると 光るのが分かります また 形式は配列なので ean8など 他のリクエストも複数設定可能です こう操作すれば ean8とQRコードの 両方が選択できます 全てを指定したい際は どうでしょう? その時は 空配列とすれば 全てのシンボロジーを 一度に読み取れるのです 全てのコードが ハイライトされているのが 見えますね ではプレゼンを進めます バーコードの読み取りに続いて 文字認識について お話いたします Visionは2019年に 文字認識を導入し 現在はファストとアキュレート 2つのモードが存在します 当初以来 Visionは 対応言語を増強してきました 文字認識の仕組みと 言語の重要性について 説明していきましょう ファストモードでは ローマ字検出機能を 備えていますが アキュレートモードは 機械学習を活用し 単語・文章単位で 処理します
識別が終わると いずれのモードも 校閲過程を経て テキスト認知に至ります 文字認識は 言語設定に大きく左右されます ファストモードでは ドイツ語のウムラウトなど 複数のラテン文字セットが サポートされています アキュレートは 全く違った仕組みを使います 中国語などの識別は ローマ字系統の言語とは かけ離れているので。 なので 中国語の文書を 識別する場合 リクエスト内で 第一言語は中国語だと 設定する必要があります 言語設定は校閲過程にも 影響を及ぼします 言語にあった辞書を 選ぶ必要があるからです
では 文字認識における 言語設定で注意すべき点は 何でしょうか? ある程度の言語は サポートされていると 思われるかもしれませんが 確認してください supporedRecognitionLanguages()で 一覧が取れます 複数言語の 指定もできます その場合 順序が大事になります 言語特定が不明確な場合は 設定内の言語の先頭が 優先され これは アキュレートで特に顕著です 最初に指定された言語が優先される つまりユースケースにより 優先順位を調整する 必要があります
では デモをお見せしましょう サンプルコードの修正版を 使っていきますね ここに複数の言語を含む 画像がありますね Revision2 を選択すると 対応言語が確認できます 英語 フランス語などですね
ちなみに Revision1にすると 英語しかなかったことが 分かりますね ファスト アキュレートに 関係なくです またRevision2で
ドイツ語の設定にすると ウムラウトが反映されました お気づきでしたか? Grüsse aus Cupertino の所です
しかし 中国語は ファストモードにありませんね
アキュレートモードなら
選択肢にあります これでやっと 「世界」の中国語文が!
ではまたスライドに 最後は ドキュメント検出についてです Visonの新しいリクエストです VNDocumentSegmentationRequest 機械学習ベースの検出器で 印刷物 看板 メモ レシピ ラベルなどを 学習したものです
リクエストに対するリザルトは 低解像度のセグメントマスクで 各ピクセルが 検知したドキュメントに 含まれる確率を返します さらに四辺形の4つの角を 検知する機能も備わっています
Neural Engine内蔵のデバイスなら カメラ入力やビデオフィードに対して リアルタイムで処理できます Vision Kitの VNDocumentCameraは Neural Engine搭載の 最新機種では VNDetectRectanglesRequestから こちらに切り替えました
VNDetectRectanglesRequest といえば この2つのリクエストは どちらも文書検知に使われますが どう違うのでしょうか? DetectDocumentsRequestは 機械学習ベースで Neural Engineに最適です CPU GPUでも作動しますが リアルタイム動作には不十分です
長方形検出器は 伝統的なコンピュータービジョンの アルゴリズムで CPU上でしか動作しません CPUがフル稼働でなければ リアルタイムでの検出が可能です
セグメンテーションリクエストは 多様な書類でトレーニングされたので 対象は長方形に限らず それが大きな強みとなります 長方形検出はというと 四辺形から 角や交錯を見つけ出しますが 角が不明瞭な場合や 折り目がある場合 認識が困難です ドキュメントリクエストは セグメンテーションマスクと 角の位置が得られるのに対し 長方形検出は角しか 見つけられません また ドキュメントリクエストは 一つの書類のみを検出するのに対し 長方形検出はすべての長方形 つまり書類内の長方形も 見つけるようになっています 実験してみましょう
先ほど述べた通り セグメンテーション認識は このように 一つの候補に限定 この場合はこの長方形です しかし長方形検出は いくつもの候補を 探し出します 画像内 全ての長方形です ここにもいくつかありますね どの長方形が書類かを決めるのは App次第なのです また応用編にしましょうか
WWDCでの働きぶりについて アンケートを作りたい 今回リモートなので カメラマンチームに 代わりに頼みました アンケートを 集計ができる Appを作りました どうでしょうか? 「Quickdrawは古臭い」って 確かに年季が入ったかも
次のはどうでしょう? 「Visionは面白くてためになった」
お最後は! 「Cobolが必要」 会場を間違えたようです さてプログラムは どうなっているでしょうか Playgroudで 作っておきました このほうが楽だからです もう既に 画像をアップロードして 加工をする必要があるので CIImageに入れます requestHandlerを作り 新しいー VNDetectDocument SegmentationRequest()も これを実行すると 書類がどこか 結果が戻ってきたので 次に このヘルパー機能で 元の画像を修正して ピッタリのアングルにします 丁度よくカットし アングルも最適化します ここまでは簡単です 次は何をすべきでしょうか? 次はバーコードです 四角形を検出しましょう さらに文字列も このリクエストができたら チェックをスキャン どこに入れていたか確認です よし ひとまず 準備完了 まず バーコードを 読み取ります
シンボロジーとしては QRコードを使います その内容が タイトルとなるので ドキュメントに 読み込んでいます 次は 四角形を検出します 四角検出のコードがこれです
二つの配列をつくります 全ての checkBoxImages の チェックの状態が 解析に必要です つまり全ての 長方形を検出します もちろん VNDetectRectanglesRequestです それから― 上から下に並べます 正しい順序で 結果が返ってくるために。
次は文字列の認識です
これは単純 全ての結果を保存する textBlocks を用意し VNRecognizeTextRequest を使います 実行してみましょう
documentRequestHandler を使って クロップした画像で リクエストを実行しました また見ると― ちゃんとQRコードが 写っています でも長方形が何かおかしい 一つも認知されていません では、なにをすべきか? 実は設定で 長方形検出は 画像の20%以上の大きさの 長方形しか探せないことに これを正します ここで―
minimumSize を設定します
10%ではどうでしょうか?
ほら 長方形が出てきました しかし一つだけ もう一つ 覚えておくべきは 検出する数が 固定されていることです デフォルト設定のせいで 長方形検出は 最も大きい長方形しか 認識していません 全てスキャンするために maximumObservations を0にします そうしたら― これでやっと チェックボックスと バーコード 長方形が読み取れました 成功です これで最後です チェックボックスを 読み取ります このために 機械学習のモデルを
作りました 事前にCreate MLで学習したものです これは画像分類器で 私はただ チェック有り・チェック無しの チェックボックスの画像を用意して 「はい」「いいえ」のレベルで モデルをトレーニングしました 読み取り不能なものも 混じらせて 「どちらでもない」に
これを コードの中で使います
さてどうなったでしょうか モデルをロードし Create MLのリクエストを作成 チェックポックスの画像それぞれに ImageRequestHandler を作成し 回答を分類します さて、一番目のボックスが 「はい」なら 横の文字列と照会して… どうなったでしょうか? ずいぶん頑張りましたが 「Visionは面白く ためになった」って! ではまたスライドを
復習しましょう 今後Vision API は文書識別に力を入れます バーコード読み取りは スキャナよりVisionが速いです 文書のセグメンテーション検出を 投入しました OCRの使い方をもっと知りたい方は WWDC 2019のプレゼンを ご覧ください WWDC 2020の "Vision and Core Image"も 独自の文書解析に役立ちます 特に自分で行いたい場合や 輪郭認知や事前加工に 興味がある方なら是非 ありがとうございました WOCCを楽しんで! [音楽]
-
-
6:18 - Barcode Scan
import Foundation import Vision let url = URL(fileReferenceLiteralResourceName: "codeall_4.png") as CFURL guard let imageSource = CGImageSourceCreateWithURL(url, nil), let barcodeImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) else { fatalError("Unable to create barcode image.") } let imageRequestHandler = VNImageRequestHandler(cgImage: barcodeImage) let detectBarcodesRequest = VNDetectBarcodesRequest() detectBarcodesRequest.revision = VNDetectBarcodesRequestRevision2 detectBarcodesRequest.symbologies = [.codabar] try imageRequestHandler.perform([detectBarcodesRequest]) if let detectedBarcodes = detectBarcodesRequest.results { drawBarcodes(detectedBarcodes, sourceImage: barcodeImage) detectedBarcodes.forEach { print($0.payloadStringValue ?? "") } } public func createCGPathForTopLeftCCWQuadrilateral(_ topLeft: CGPoint, _ bottomLeft: CGPoint, _ bottomRight: CGPoint, _ topRight: CGPoint, _ transform: CGAffineTransform) -> CGPath { let path = CGMutablePath() path.move(to: topLeft, transform: transform) path.addLine(to: bottomLeft, transform: transform) path.addLine(to: bottomRight, transform: transform) path.addLine(to: topRight, transform: transform) path.addLine(to: topLeft, transform: transform) path.closeSubpath() return path } public func drawBarcodes(_ observations: [VNBarcodeObservation], sourceImage: CGImage) -> CGImage? { let size = CGSize(width: sourceImage.width, height: sourceImage.height) let imageSpaceTransform = CGAffineTransform(scaleX:size.width, y:size.height) let colorSpace = CGColorSpace.init(name: CGColorSpace.sRGB) let cgContext = CGContext.init(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: 8 * 4 * Int(size.width), space: colorSpace!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)! cgContext.setStrokeColor(CGColor.init(srgbRed: 1.0, green: 0.0, blue: 0.0, alpha: 0.7)) cgContext.setLineWidth(25.0) cgContext.draw(sourceImage, in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) for currentObservation in observations { let path = createCGPathForTopLeftCCWQuadrilateral(currentObservation.topLeft, currentObservation.bottomLeft, currentObservation.bottomRight, currentObservation.topRight, imageSpaceTransform) cgContext.addPath(path) cgContext.strokePath() } return cgContext.makeImage() }
-
14:02 - Survey Scan
import Foundation import CoreImage import Vision import CoreML guard var inputImage = CIImage(contentsOf: #fileLiteral(resourceName: "IMG_0001.HEIC")) else { fatalError("image not found") } inputImage let requestHandler = VNImageRequestHandler(ciImage: inputImage) let documentDetectionRequest = VNDetectDocumentSegmentationRequest() try requestHandler.perform([documentDetectionRequest]) guard let document = documentDetectionRequest.results?.first, let documentImage = perspectiveCorrectedImage(from: inputImage, rectangleObservation: document) else { fatalError("Unable to get document image.") } documentImage let documentRequestHandler = VNImageRequestHandler(ciImage: documentImage) /* TODO Detect barcodes Detect rectangles Recognize text Perform those requests Scan checkboxes */ var documentTitle = "Don't know yet" let barcodesDetection = VNDetectBarcodesRequest() { request, _ in guard let result = request.results?.first as? VNBarcodeObservation, let payload = result.payloadStringValue else { return } documentTitle = "\(payload) was: " } barcodesDetection.symbologies = [.qr] var checkBoxImages: [CIImage] = [] var rectangles: [VNRectangleObservation] = [] let rectanglesDetection = VNDetectRectanglesRequest { request, error in rectangles = request.results as! [VNRectangleObservation] // sort by vertical coordinates rectangles.sort{$0.boundingBox.origin.y > $1.boundingBox.origin.y} for rectangle in rectangles { guard let checkBoxImage = perspectiveCorrectedImage(from: documentImage, rectangleObservation: rectangle) else { print("Could not extract document"); return } checkBoxImages.append(checkBoxImage) } } rectanglesDetection.minimumSize = 0.1 rectanglesDetection.maximumObservations = 0 var textBlocks: [VNRecognizedTextObservation] = [] let ocrRequest = VNRecognizeTextRequest { request, error in textBlocks = request.results as! [VNRecognizedTextObservation] } do { try documentRequestHandler.perform([ocrRequest, rectanglesDetection, barcodesDetection]) } catch { print(error) } let classificationRequest = createclassificationRequest() var index = 0 for checkBoxImage in checkBoxImages { let checkBoxRequestHandler = VNImageRequestHandler(ciImage: checkBoxImage) do { try checkBoxRequestHandler.perform([classificationRequest]) if let classifications = classificationRequest.results as? [VNClassificationObservation] { if let topClassification = classifications.first { if topClassification.identifier == "Yes" && topClassification.confidence >= 0.9 { for currentText in textBlocks { if observationLinesUp(rectangles[index], with: currentText) { let foundTextObservation = currentText.topCandidates(1) documentTitle += foundTextObservation.first!.string + " " } } } } } } catch { print(error) } index += 1 } print(documentTitle) extension CGPoint { func scaled(to size: CGSize) -> CGPoint { return CGPoint(x: self.x * size.width, y: self.y * size.height) } } extension CGRect { func scaled(to size: CGSize) -> CGRect { return CGRect( x: self.origin.x * size.width, y: self.origin.y * size.height, width: self.size.width * size.width, height: self.size.height * size.height ) } } public func observationLinesUp(_ observation: VNRectangleObservation, with textObservation: VNRecognizedTextObservation ) -> Bool { // calculate center let midPoint = CGPoint(x:textObservation.boundingBox.midX, y:observation.boundingBox.midY) return textObservation.boundingBox.contains(midPoint) } public func perspectiveCorrectedImage(from inputImage: CIImage, rectangleObservation: VNRectangleObservation ) -> CIImage? { let imageSize = inputImage.extent.size // Verify detected rectangle is valid. let boundingBox = rectangleObservation.boundingBox.scaled(to: imageSize) guard inputImage.extent.contains(boundingBox) else { print("invalid detected rectangle"); return nil} // Rectify the detected image and reduce it to inverted grayscale for applying model. let topLeft = rectangleObservation.topLeft.scaled(to: imageSize) let topRight = rectangleObservation.topRight.scaled(to: imageSize) let bottomLeft = rectangleObservation.bottomLeft.scaled(to: imageSize) let bottomRight = rectangleObservation.bottomRight.scaled(to: imageSize) let correctedImage = inputImage .cropped(to: boundingBox) .applyingFilter("CIPerspectiveCorrection", parameters: [ "inputTopLeft": CIVector(cgPoint: topLeft), "inputTopRight": CIVector(cgPoint: topRight), "inputBottomLeft": CIVector(cgPoint: bottomLeft), "inputBottomRight": CIVector(cgPoint: bottomRight) ]) return correctedImage } public func createclassificationRequest() -> VNCoreMLRequest { let classificationRequest: VNCoreMLRequest = { // Load the ML model through its generated class and create a Vision request for it. do { let coreMLModel = try MLModel(contentsOf: #fileLiteral(resourceName: "CheckboxClassifier.mlmodelc")) let model = try VNCoreMLModel(for: coreMLModel) return VNCoreMLRequest(model: model) } catch { fatalError("can't load Vision ML model: \(error)") } }() return classificationRequest }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。