大多数浏览器和
Developer App 均支持流媒体播放。
-
用 Vision 提取文稿数据
了解 Vision 如何在您的 app 中提供专业图像识别和分析,以提取文档信息、识别各种语言的文本、以及识别条形码。我们将探索文本识别和二维码检测的最近更新,向您展示如何将所有这些工具与 Core ML 整合,帮助您的 app 通过图像和实时摄像头更好地感知世界。要了解更多关于 Vision 的信息,请观看 WWDC21 的“用 Vision 检测人员、人脸和姿势”和 WWDC20“探索计算机视觉 API”。要深入理解 Vision 的各种功能,请观看 WWDC21 的“用 Vision 检测人员、人脸和姿势”和 WWDC20 的“探索计算机视觉 API”。
资源
相关视频
WWDC21
WWDC20
WWDC19
-
下载
你好 欢迎来到WWDC 我叫弗兰克.多普克 是Vision团队的一名工程师
我们多年来一直在发展Vision框架 重点在图片分析 为了更好地掌握 我们可以从Vision的使用重点 来看它的功能 运动 追踪对象和分析身体姿势 可以帮助你 创建运动app
辅助使用 光学字符识别 或图片分类和对象侦测等视觉请求 能帮助视障用户
人 Vision提供了许多与脸部 和身体相关的请求 你的app可以使用这些请求 你可以在《使用Vision 侦测人物、脸部和姿势》 专题中找到更多相关信息
健康 从条形码扫描和光学字符识别 到身体姿势分析 Vision提供构建模块 来创建智能健康app
计算摄影 人像模式等功能 依赖于人脸侦测和分割
安全性 脸部和身体检测等请求 有助于监控摄影机中的 动作检测等app
和文件 这就是我们希望在本次会议中 关注的内容
Vision提供了许多 帮助你分析文档的请求: 条形码检测 文字辨识 或者俗称光学字符识别 轮廓检测 矩形检测 以及今年新增的 文档分割检测
以下是这次的议程 首先 我们将讨论条形码检测 然后 我们将讨论文字辨识 最后 我们将讨论文档检测
让我们来看看条形码检测 今年 我们推出了条形码检测 要求的新的API版本参数
VNDetectBarcodesRequestRevision2 提供对新符号的支持 二进制级别条形码 全方向条形码 包括扩展和限制版 MicroPDF和MicroQR 对后者特别有用 如果你想为网址制作QR码 并需要放置在小标签或包装上 因为它占用的空间要少得多
我们改变了这个新版本的行为 使其与Vision的其余部分保持一致 即根据客户端指定的 感兴趣区域报告 生成的边界框
让我们详细看看这种变化 在这里 我们有一个带有QR码的文档 当我们不指定感兴趣区域 也称为ROI 边界框将根据完整图片 进行报告 现在 让我们指定一个感兴趣区域 就像我们只想关注相机 看到的中心部分一样 修订版二现在报告 与感兴趣区域相关的 边界框 就像其他Vision请求一样
不幸的是 版本1的API 总是与完整图片相关 但我们不想改变这种行为 因为它可能会破坏现有的客户端 提醒一下 当你针对最新的软件开发工具包 编译你的app 并且未指定特定版本时 你将始终根据你的请求 获得最新修订版 但是对于指定版本1 或不针对新软件开发工具包 重新编译的app 它们仍将获得旧的版本1的行为
让我重点介绍Vision中 条形码侦测请求的一些有趣方面 Vision支持一维和二维条形码
但真正有趣的是 在一张图片中 它可以同时侦测多个代码 以及多个符号
这意味着你不必 一次又一次地扫描以获得多个代码 与大多数手持式扫描仪相比 这是一个巨大的优势 请记住 如果你扫描多个符号系统 则指定的符号系统越多 所需的时间就越长 因此 你只想使用与你的用例 相关的符号来设置请求
随着条形码扫描 新符号体系的扩展 Vision可以在卫生部门 发挥特别有用的作用 使用iPhone 你可以一次分析多个代码 并且由于它与互联网的连接 无需单独的扫描仪即可提取信息 得益于iPhone强大的弱光功能 即使在黑暗的场景中 你也可以扫描代码 而无需发射激光 或在患者休息时打扰他们
现在 让我们看看Vision 如何执行条形码检测
一维代码将被扫描为线 这代表你可能会针对 同一代码进行多次检测 通过查看有效载荷 包含在条形码中的真实数据 可以轻松地对它们 进行重复数据删除
QR码作为一个单元进行扫描 这代表你将获得整个代码的 一个边界框 二维码的一个例子是QR码
每个条形码都会有自己的观察报告 但正如我之前提到的 一维代码可以返回多个观测值 具有相同的内容 但位于不同的物理位置 有效载荷是条形码的内容 即包含在这个机器 可读代码中的数据 特别是对于QR码的有效负载 你可能希望使用数据侦测器 来分析编码的网址
现在 让我们在这个小示例中看一下
好的 这是Xcode Playground 你会看到我有一张 包含所有条形码的图片 我使用 VNDetectBarcodesRequest 并将修订版设置为二 现在 作为符号系统 我只有二进制级别条形码 当我们查看时 我们看到二进制级别条形码以红色显示
现在 我可以将其更改为QR码
然后 我们再次执行请求 我们看到QR码被特别显示 但它是一个数组 所以我也可以用它 指定其他请求 比如说ean8条形码 当我这样做时 我们现在会看到我们同时拥有 ean8条形码和QR码 但是如果我全都想要怎么办? 我只要输入一个空数组 就这样所有符号都能被读取 正如你所看到的 下方所有代码都有了加强显示 让我们回到我们的幻灯片 从条形码 我们现在开始研究 文字辨识 Vision在2019年引入了文字辨识 它以两种模式运行:快速和准确 从那时起 Vision扩大了语言支持 让我们看看文字辨识的工作原理 以及语言在何处发挥作用 在快速路径中 我们有一个拉丁字符识别器 另一方面 准确路径中 使用基于机器学习的识别器 来处理单词和行
识别完成后 每条路径都要经过语言修正阶段 并且 最后 我们得到了识别的文字 语言选择会影响识别阶段 在快速路径中 这代表支持不同的 拉丁字符集 例如德语的变音符号 在准确路径中 当我们必须识别中文时 会使用完全不同的模型 因为它的结构 与基于拉丁语的语言非常不同 这意味着如果你需要阅读中文字 那么调整请求 将中文设为主要语言 这点很重要 语言选择也会影响语言修正 因为它会为工作选择正确的词典
那么 在文字辨识中 使用语言的最佳实践是什么? 尽管看起来似乎支持一组 固定的语言 但最好使用 supportedRecognitionLanguages() 查询配置 支持哪些语言 你可以指定多种语言 在这种情况下 顺序很重要 当有歧义时 它会按语言顺序解决 特别是对于准确路径 第一种语言决定使用哪种识别模型 这意味着你的用例决定了 你想在请求中使用哪些语言
让我们来看这个小小示范 所以 现在这里是 示范代码的修订版 你可以看到我有一张 包含不同语言文字的图片 现在 我指定了修订版二 我可以看到支持哪些语言 我们有英语、法语等
现在 例如 如果我切换回修订版一 我们可以看到我只有英语 这对于快速 和准确路径是一样的 现在 让我们回到修订版二
请注意 当我现在切换到 假设是德语时 实际上我在 Grüsseaus Cupertino中 正确地获得了变音符号
但是我没有 中文的快速路径的支持
在准确路径中
我现在可以选择中文 现在 我们终于得到了 “Hello World”的正确中文字
让我们回到幻灯片 最后但同样重要的 让我们看看文档侦测 Vision引入了一个新请求 名为 VNDocumentSegmentationRequest 这是一个基于机器学习的侦测器 我们已经对各种类型的文件 进行了训练 例如纸张、标志 笔记、收据、标签等
请求的结果 是一个低解析率的遮罩 其中每个像素代表一个置信度 无论该像素 是否为被侦测文档的一部分 此外 它还提供了四边形的 四个角点
在带有神经网络引擎的设备上 请求可以在拍照或摄影同时运行 VisionKit中的 VNDocumentCamera 现在在带有神经网络引擎的 现代设备上使用请求 而不是VNDetectRectanglesRequest
说到 VNDetectRectanglesRequest 这两个请求有何不同 不是都可以用于侦测文档吗? 正如我所提到的DetectDocumentsRequest 是基于机器学习的 并且在神经网络引擎上 执行速度最快 但它也可以在GPU 或CPU上使用 但在实时性能方面 速度不够快
矩形侦测器 是一种传统的计算机视觉算法 只在CPU上运作 只要CPU没有被其他任务饱和 就可以跟上实时性能
文档请求是针对各种文档进行训练的 它们不必都是矩形 这是其主要优势之一 另一方面 矩形侦测器 通过查找形成四边形的 边和交点来运作 这对于文档中的角落 或折叠处被遮住可能是一个挑战 文档请求提供了分割遮罩 和角点 而矩形侦测器仅提供角点 并且文档侦测器 被训练为仅查找一份文档 使用矩形侦测器将返回多个矩形 这些矩形甚至可以嵌套 让我们多看一点
正如我所提到的 文档侦测器找到一个文档 我们在这里看到的是 侦测对象的四边形 但是矩形侦测器 会返回它在图片中 找到的所有矩形的多个观察结果 我在图片上有特别强调 这是由app决定 哪个矩形是文档 不如我们在示范中一起试试看吧?
好的 我们想做个小调查 了解我们在WWDC上的表现 但很遗憾 你们不在我身边 所以我只好请这里的摄影团队 替你们填写调查问卷 所以 我做了一个小app 我现在可以扫描我们的调查卡 结果如何? 对于初学者来说 限时涂鸦感觉已经过时了 的确有点老了
让我们换下一张 Vision既有趣又能增长见闻
最后但同样重要的 通用商业语言 太好了 有人加入了错误的课程 好 现在让我们看看 是如何在代码中做到这一点的 所以 我在这里又建了一个playground 因为在这里构建这些东西更为容易 你已经看到的是我加载了一张图片 我使用了CIImage 因为我需要对它进行一些图片处理 我创建了一个requestHandler 并使用了新的 VNDetectDocument- SegmentationRequest() 一旦我执行请求 我就能得到结果 我建了一个小辅助函数 我现在使用核心图片 将用作预期的校正图片 我们得到一张裁剪后的 预期校正形式的卡片 很简单 那我们接下来要怎么做呢? 我们需要检测条形码 检测矩形 并辨识文字 执行此请求后 我们必须扫描复选框 以查看勾选了哪个复选框 好的 我稍微准备了一下 让我们从侦测条形码开始
我使用的符号系统 只有QR码 我加载到文档标题中 因为我知道 我QR码的内容 就会是我们从中得到的内容标题 接下来 我们需要侦测矩形 同样 我们有一小段代码矩形
所以 我创建了两个数组 我想获取所有的checkBoxImages 这是分析所需的数据 我把所有的矩形都拿出来了 所以 我使用了 VNDetectRectanglesRequest 现在 我在这里做的是 按垂直顺序对它们进行排序 以便以正确的顺序得到结果
好的 现在我们需要识别我们的文字
很简单 我们存储所有生成的textBlocks 并使用VNRecognizeTextRequest 所以现在我们要做的 就是简单地执行请求
正如你所看到的 我使用了 documentRequestHandler 就是用来裁剪图片的 并对其执行请求 如果我回到这里 我就可以看到我得到了正确的QR码 但是我的矩形有些不太对劲 我没有得到任何矩形 那我该怎么办呢? 默认情况下 矩形侦测器只查找 至少占图片20%的矩形 所以 我们需要纠正这一点 所以 我要键入
并将minimumSize设置为
比如说 像10%这样
一旦我们这样做了 我们就会得到一个矩形 好吧 这里只有一个 矩形侦测器的另一件事是 我需要告诉它应该回传多少 默认情况下 矩形侦测将只会回传一个 也就是最突出的矩形 但我想得到所有矩形 我通过将maximumObservations 设置为0来做到这一点 一旦我这样做了 我现在就能得到 所有的复选框和条形码 因为看起来都像矩形 好 很好 现在到了最后的部分 我需要实际扫描复选框 因此 我实际上准备了 一个机器学习的小示例
我这里有个模型 我之前用Create ML训练过 这是一个图片分类器 我所做的只有 使用了其中一些复选框图片 有些有标记 有些没标记 用在我的“是”和“否”标签 我还收集了一些不是复选框的图片 当作错误图片
我们一样可以在我们的代码中使用
那我们有什么? 我们通过加载模型 来创建我们的请求 并创建我们的Create ML请求 然后我们反复所有的复选框图片 从中创建一个 ImageRequestHandler 并执行我们的分类 现在 我可以查看我的顶层分类 如果那是“是” 那么我就能找到 跟我有的复选框对齐的文字 我们最后会得到什么? Vision有趣 又能增长见闻 让我们回到幻灯片
让我们复习一下刚才看到的 文档分析 是Vision应用程序编程接口的一个重点 Vision中的条形码检测 比扫描仪更通用 我们正在引入新的文档分割检测 如果你想了解有关 如何使用光学字符识别的更多信息 请查看我们的WWDC 2019讲座 WWDC 2020的 《Vision和Core Image》讲座 能为你提供更多观点 帮助你进行自定义文档分析 用的是前处理图片 和侦检测轮廓 谢谢你 好好享受WWDC的其他专题 [音乐]
-
-
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 }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。