大多数浏览器和
Developer App 均支持流媒体播放。
-
探索计算机视觉 API
学习如何将计算机视觉只能添加到你的 app,将 Core Image,Vision 与 Core ML 的力量结合起来。超越单纯的机器学习技术,对图片与视频产生更深层次的理解。探索 Core Image 与 Vision 的全新 API,以类似 Contour Detection 与 Optical Flow 的方式,将计算机视觉以全新阈值过滤器的方式加入你的 app。 要了解更多关于相关基础框架的内容,请查看“视觉框架:以 Core ML 为基础”与“Core Image:性能,原型开发与 Python”。若要进一步探索计算机视觉 API,请查看[“在 Vision 中检测身体与手部姿势”]与[“探索 Action 与 Vision app”]等节。
资源
相关视频
WWDC21
WWDC20
WWDC19
-
下载
(你好 WWDC 2020) 你好 欢迎参加 WWDC
欢迎参加 WWDC 我叫 Frank Doepke 跟我的同事 David Hayward 一起 我们将探索计算机视觉 API
为什么要谈论计算机视觉? 计算机视觉可以增强你的 app 即便这不是你的业务核心 它会为你的 app 带来一些新东西
让我给你举一个例子
银行 app 使你能兑现支票 它们利用计算机视觉让相机为你读取支票 因此你不必再输入信息 显然 计算机视觉不是银行业的核心 但通过这样做 你可以为用户省去很多步骤 他们不必再输入任何东西
另一件事可能是 你想读取二维码 或者读收据 所有这些可能都不是你的 app 的核心 但使用相机使你的用户 能更轻松地做到这一点
我们有什么 API 可以用于 计算机视觉呢?
在最高层 我们有 VisionKit 它是 VNDocumentCamera 的所在地 你可能在备忘录或信息或邮件中 看到过它扫描文档 然后我们用 Core Image 对图像进行图像处理 用计算机视觉分析图像 最后 用 Core ML 进行机器学习推理 今天我们只关注 Core Image 和计算机视觉 但我想确保你不要将它们视为 并排而立的柱子 它们实际上可以紧密相连 我可能想进行一些图像预处理 将其在计算机视觉中运行 从那里获取结果 将其导入 Core ML 或返回到 Core Image 以创建一些效果 要谈论如何用 Core Image 为计算机视觉预处理图像 我想交给我的同事 David Hayward
谢谢 Frank 我想借此机会介绍一下 如何用 Core Image 改进计算机视觉算法
如果你对 Core Image 不熟悉 它是以 Metal 为基础的 优化型易用图像处理框架 要深入了解它的工作原理 我建议你观看 WWDC 2017 专题介绍
你的 app 为什么应该使用 Core Image 和计算机视觉有两个主要原因
用 Core Image 对计算机视觉输入 进行预处理 可以使算法更快、更稳健 (使你的图像更易于分析) 用 Core Image 对计算机视觉输出 进行后处理 可以为你的 app 提供新的方式 来向用户显示这些结果 (使分析结果可视化) 同时 Core Image 是为机器学习训练 进行增强的绝佳工具 在 WWDC 2018 的介绍中 有一些很好的例子 (CORE IMAGE:性能、原型和 PYTHON) (缩放) 准备要分析的图像的最佳方法之一是 缩小图像以获得最佳性能
整体质量最好的缩放器是 CILanczosScale
在代码中使用这个滤镜非常容易 只需要导入 CIFilterBuiltins 标头 创建滤镜实例 设置输入属性 然后获取 outputImage 就这么简单
但这只是 Core Image 中的 几个重采样滤镜之一 取决于你的算法 可能最好使用线性插值 CIAffineTransform
形态学操作是一种很好的技术 可以使图像中的小特征更加突出
用 CIMorphologyRectangleMaximum 进行膨胀 会使图像的较亮区域变大
用 CIMorphologyRectangleMinimum 进行腐蚀 会使这些区域变小
更好的方法是用 CIMorphologyRectangleMinimum 然后是 CIMorphology- RectangleMaximum 进行关闭 这对去除图像中可能影响算法的 小区域噪声非常有用
有些算法只需要单色输入 在这种情况下 计算机视觉会自动将 RGB 转换成灰度 如果你有关于输入图像的领域知识 那么用 Core Image 转换成灰色的结果可能会更好
通过 CIColorMatrix 你可以为 本转换指定任何加权
或者通过 CIMaximumComponent 将使用信号最好的信道
在进行图像分析之前降噪也值得考虑
通过几次 CIMedianFilter 可在不软化边缘的情况下降噪
CIGaussianBlur 和 CIBoxBlur 也是快速降噪的方法
也考虑使用 CINoiseReduction 滤镜
Core Image 还有各种边缘检测滤镜
对于 Sobel 边缘检测器 可以使用 CIConvolution3X3
更好的方法是使用 CIGaborGradients 这会产生一个 2D 梯度向量 也更能容忍噪声
增强图像的对比度可能有助于对象检测
CIColorPolynomial 使你能指定 任意三阶对比度函数 CIColorControls 提供线性对比度参数
今年 Core Image 也有一些新的滤镜 可以将你的图像转换成黑白色
例如 CIColorThreshold 使你能在 app 代码中设置阈值 而 CIColorThresholdOtsu 会根据图像的直方图自动确定最佳阈值
Core Image 还有 用于比较两个图像的滤镜 这有助于为检测视频帧间运动做好准备
例如 CIColorAbsoluteDifference 是今年的新滤镜 对这有帮助
此外 CILabDeltaE 会用 旨在匹配人类对颜色的感知 的公式来比较两个图像
这些只是 Core Image 中 200 多个内置滤镜中的一些
为了帮助你使用这些内置滤镜 本说明文档包括参数描述 示例图像 甚至示例代码
如果这些过滤器都不满足你的需求 那么你可以用 Metal Core Image 轻松编写自己的滤镜 我们建议你参阅我们今年发布的 关于这个问题的课程
对于图像处理和计算机视觉 务必注意图像 可能有各种彩色空间
你的 app 可能会收到从传统 sRGB 到宽色域 P3 空间中的图像 现在甚至支持 HDR 彩色空间
你的 app 应该为各种彩色空间做好准备 好消息是 Core Image 使这非常容易
Core Image 自动将输入 转换到其工作空间 Unclamped、Linear、BT.709 primaries
但你的算法可能需要不同彩色空间的图像 在这种情况下 你应该执行以下操作
你需要从 CGColorSpace 获取 要使用的彩色空间的变量 你会调用 imagematchFromWorkingSpace
在该空间应用算法 然后调用 imagematchtoWorkingSpace
这样就可以了 我今天的最后一个话题是用 Core Image 对计算机视觉输出进行后处理 这方面的一个例子是用 Core Image 从计算机视觉条码 observation 重新生成条码图像
你只需要在代码中创建滤镜实例
将其 barcodeDescriptor 属性设置为 计算机视觉 observation 的属性 最后获得输出图像 结果如下
同样 你的 app 可以基于计算机视觉 face observation 应用滤镜
举例来说 你可以用这种方法 轻松使用晕影效果
代码其实非常简单 需要注意的一件事是 你需要从计算机视觉的标准化坐标系 转换为 Core Image 的笛卡尔坐标系
创建晕影滤镜后 你就可以用 compostingover 在图像上覆盖晕影
你还可以用 Core Image 使向量场可视化 Frank 稍后会进行演示
我的介绍到此结束 下面由 Frank 进一步谈论计算机视觉
好的 谢谢 David 现在我要谈谈如何通过 使用计算机视觉来理解图像
我们有任务、机器和结果 任务是你想做的事 机器是执行任务的工具 结果当然就是你想要的 你想得到的
任务可能在我们的编译程序 VNRequests 中 例如 VNDetectFaceRectanglesRequest 机器是这两个中的一个 我们有 ImageRequestHandler 或 SequenceRequestHandler
我们得到的结果被称为 VNObservation 这取决于你执行的任务 例如用于检测矩形的 VNRectangleObservation
我们首先执行 ImageRequestHandler 上的请求 我们从那里获得 observation 让我们看一个具体的例子
我们想读取文本 因此我们使用 VNRecognizeTextRequest
然后我用图像创建 ImageRequestHandler
从中我得到了 observation 只是纯文本
计算机视觉在 2020 年有什么新功能?
首先 我们有手势和位姿 要了解更多 请参阅“手势和位姿”会议
然后你可能看过我们的轨迹检测 关于这一点 你可以在 “探索动作与计算机视觉 app” 中查看更多信息
今天 我们要关注轮廓检测和光流
什么是轮廓检测? 通过轮廓检测 我可以找到图像的边缘
正如我们在这里看到的 红线现在显示 我们在这张图形中找到的轮廓
我们从图像开始 然后创建 VNDetectContourRequest
我们现在可以设置图像的对比度 以进行增强 例如 一些对比度如何突显 我们可以在两者之间切换 我们想用这个浅色背景 在深色背景下运行吗? 这样可以区分前景和背景?
最后 我们可以添加最大图像尺寸 这使你能权衡性能和精确性
这意味着 例如 如果你以较低的分辨率看它 你仍然可以看到轮廓 但轮廓可能不会精确地沿着边缘 但运行得更快 因为它能以较低的分辨率运行 相比之下 当我们使用较高的分辨率时 在进行一些后处理时你可能需要这样做 我们会得到更精确的轮廓 但这需要更长的时间 因为需要做更多工作
让我们看看我们得到的 observation
这里我们有一个非常简单的图像 两个正方形 里面有一个圆形
我们得到了 VNContoursObservation
topLevelContours 是我们看到的两个矩形
里面是 childContour 它们是嵌套的 这些是圆形
然后我们得到 contourCount 我可以用来穿过所有轮廓
但使用诸如索引路径要容易得多 你可以看到 它们相互嵌套 我现在可以遍历图形
最后 我还得到 normalizedPath 这是一个 CGPath 可以轻松地用于渲染
现在 什么是 VNContour? 在例子中 我们得到了 VNContour
这是最外部的轮廓 我们的父轮廓 嵌套在里面的是 childContour 这些是内部轮廓
我的轮廓有索引路径 当然 每个 childContour 都有索引路径 我可以用来再次遍历图形
然后我得到 pointCount 中的 normalizedPoint 这是轮廓最重要的部分 因为它描述我们找到的每个线段 因为我们不仅找到了像素 我们得到了一个轮廓 一条路径
我们还有一个 aspectRatio 我会在下一张幻灯片上 讨论这个问题
然后我们有要渲染的 normalizedPath 在处理轮廓时 我们需要记住几点 让我们来看看这张图像
它是1920x1080 像素 中间有一个高和宽均为 1080 像素的圆形
但计算机视觉使用标准化坐标空间 因此我们的图像是高 1.0 宽 1.0 因此 圆形的高度现在是 1.0 但宽度是 0.5625 如果你想考虑到 你检测到的形状的几何结构 你需要看看计算它的原始图像的 aspectRatio
在能对其进行分析时 轮廓会很有趣 而且我们有几个实用程序可供你使用
VNGeometryUtils 提供一些 API 例如 我们有 boundingCircle 它是完全封装你检测到的轮廓的 最小的圆形 很适合用来比较轮廓
然后我们可以计算面积 而且我们可以计算周长
接下来你需要做的是 简化轮廓 当我们从图像得到轮廓时 它们往往有噪声 让我们看看这个例子
我们有一个拍摄的矩形
但你可以看到 它有一些小弯 轮廓是沿着这些小弯的 现在我没有边角的所有点 甚至没有中间的所有点
我现在可以通过用 Epsilon 使用多边形的近似值 Epsilon 意味着我可以滤除 边缘周围有噪声的小区域 这样只有分明的轮廓边缘才会保留
现在 我再次得到一个完美的矩形 我只有四个点 因此如果我需要分析形状 这对我来说非常简单 因为我可以说 “如果它有四个点 那么它是四边形” 并且我检测到它是什么形状
我们来看一个具体的例子 说明如何使用所有这些 (计算机视觉问题到原生解决方案)
假设我们需要通过 恢复在穿孔卡片机上完成的 非常老的计算机代码来拯救世界
我们的任务是识别穿孔卡片上的小坑 因为没人有穿孔卡片阅读器了
我们在网上搜索 找到关于如何做到这一点的 计算机视觉博客帖子 但它是用 Python 编写的 我们的任务当然是 将其以原生状态带到我们的平台上 这样我们就能以最好的方式运行它
现在我们有一段 Python 代码 如果你不懂 Python 别担心 我会简要地为你介绍一下 理念通常都是一样的 我们先进行一些图像处理
然后进行一些图像分析
得到一些需要可视化的结果 即便你不理解 Python 我想在一开始就向你强调一点 你看到的前三行 可以看到 我们需要导入几个库 Python 没有库 这些是你需要包含的第三方库
我们如何原生地做到这一点?
对于图像处理部分 我们需要加载图像 你已经知道该怎么做了 使用 CGImageSource 从中获取一个用户界面图像 将其加载到 CIImage… 给它命名 然后你可以通过使用 CIFilter 来用 Core Image 处理图像 就像在 CIAbsoluteThreshold 中一样 或者是 David 前面说过的许多其它方法
现在我们要进行图像分析 为此 我们从刚刚处理的 CIImage 创建 ImageRequestHandler 然后我们执行请求 例如 VNDetectContourRequest 这个请求的好处是 我们甚至不必预处理图像
然后我们使结果可视化 同样 我们可以用 Core Image 来做到这一点 这使我们能将其直接叠加到我们拥有的 在同一背景下的图像 你可以使用 CIMeshGenerator 或 CITextGenerator
但我也可以使用 CoreGraphics 或 UIKit 将其渲染到图像上方的层 好的 看过所有这些幻灯片之后 让我们看一个真正的演示 让我转到我的演示设备
我准备的是一个小“游乐场” 你看到我已经加载了图像
我创建了 contourRequest…
然后我执行它 然后就好了 我可以看到所有轮廓 包括我在找的小坑 注意 我找到了 387 个轮廓 这可能比我想要的多
我们需要滤除所有这些轮廓
我做了一些准备 而且我隐藏了一些代码 让我揭示这段代码 以及所有的一切… 由于我的领域知识 我知道轮廓在一个蓝色背景上 现在 用一些 CIFiltering 先将所有噪声模糊
然后用颜色控件来显示对比度 然后用轮廓探测 运行过滤后的图像 现在你看到 我只找到 32 个轮廓 这是我起初在意的小坑 好的 让我们回到幻灯片
通常 我会在演示中谈论我做了什么 但实际上 更重要的是我不必做什么
你注意到了 我没有加载任何第三方软件包 因为这都是 OS 的一部分 我只使用了 UIKit Core Image 和计算机视觉
通过使用最佳处理路径 我也从来没离开图像管道 因为我停留在管道内
没有将图像转换成矩阵 由此 我节省了内存 也节省了大量计算成本
这是轮廓检测 接下来 我们来看看光流 什么是光流?
我们要分析两个帧之间的运动
传统上 我们可能会使用注册 这在很长一段时间内 是计算机视觉的一部分 它使整张图像对齐 让我们看一个例子
我们有这两个点 我们来看看这是不是用相机拍的照片 然后我们移动了相机
现在 这两个点移到了顶部和右侧
通过告诉我图像向上和向右移动了多少 注册会使我获得两个图像之间的对齐情况
而光流是不同的 它给出 X 和 Y 之间每个像素的移动 这是计算机视觉今年的新功能
在例子中 我们同样有两个点
但现在 它们分开了
图像注册不能准确地获取这一信息 但是我可以使用光流 因为它会告诉我 对于每个像素 它们是如何移动的
让我们看看光流的结果
从光流 我得到了 VNPixelBufferObservation 它是一个浮点图像 有交替的 X 和 Y 运动
当我们有这样一个视频时 你可以想象 或许仅仅看这些值本身 会很难想象发生了什么 因为它们是要在以后的算法中处理的 但是如果我想看看 我可以用 Core Image 使结果可视化 正如之前 David 在课程中提到的 这是有办法实现的 我们创建了一个小的自定义内核 现在 你可以看到一切是如何移动的 我有一个彩色编码 它显示运动的强度 小三角形显示运动的方向
让我简要地告诉你我们是怎么做到的 我们编写一个自定义滤镜 我需要加载内核 我们会在幻灯片附件中将其提供给你 然后我只需要用所需箭头尺寸的参数 应用该内核 并将其作为滤镜运行 现在 在我的计算机视觉代码中 我只需要 运行 VNGenerateOpticalFlow 请求 我得到 pixelBuffer 的 observation 我可以将其封装到 CIImage 中 然后只需要将其导入滤镜 并获取输出图像
让我们总结一下今天谈论的东西 计算机视觉并不难 而且它能增强你的 app 我们的原生 API 使其易于快速采用 通过将这些结合起来 你可以创建一些有趣的东西 我期待着你打造出绝佳的 app 和伟大创新 感谢你参加我们的会议 尽情享受 WWDC 的剩余部分
-
-
19:24 - Reading punchcards playgrounds
import UIKit import CoreImage import CoreImage.CIFilterBuiltins import Vision public func drawContours(contoursObservation: VNContoursObservation, sourceImage: CGImage) -> UIImage { let size = CGSize(width: sourceImage.width, height: sourceImage.height) let renderer = UIGraphicsImageRenderer(size: size) let renderedImage = renderer.image { (context) in let renderingContext = context.cgContext // flip the context let flipVertical = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: size.height) renderingContext.concatenate(flipVertical) // draw the original image renderingContext.draw(sourceImage, in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) renderingContext.scaleBy(x: size.width, y: size.height) renderingContext.setLineWidth(3.0 / CGFloat(size.width)) let redUIColor = UIColor.red renderingContext.setStrokeColor(redUIColor.cgColor) renderingContext.addPath(contoursObservation.normalizedPath) renderingContext.strokePath() } return renderedImage; } let context = CIContext() if let sourceImage = UIImage.init(named: "punchCard.jpg") { var inputImage = CIImage.init(cgImage: sourceImage.cgImage!) let contourRequest = VNDetectContoursRequest.init() // Uncomment the follwing section to preprocess the image // do { // let noiseReductionFilter = CIFilter.gaussianBlur() // noiseReductionFilter.radius = 1.5 // noiseReductionFilter.inputImage = inputImage // // let monochromeFilter = CIFilter.colorControls() // monochromeFilter.inputImage = noiseReductionFilter.outputImage! // monochromeFilter.contrast = 20.0 // monochromeFilter.brightness = 8 // monochromeFilter.saturation = 50 // // let filteredImage = monochromeFilter.outputImage! // // inputImage = filteredImage // } let requestHandler = VNImageRequestHandler.init(ciImage: inputImage, options: [:]) try requestHandler.perform([contourRequest]) let contoursObservation = contourRequest.results?.first as! VNContoursObservation print(contoursObservation.contourCount) _ = drawContours(contoursObservation: contoursObservation, sourceImage: sourceImage.cgImage!) } else { print("could not load image") }
-
23:05 - Optical Flow Visualizer (CI kernel)
// // OpticalFlowVisualizer.cikernel // SampleVideoCompositionWithCIFilter // kernel vec4 flowView2(sampler image, float minLen, float maxLen, float size, float tipAngle) { /// Determine the color by calculating the angle from the .xy vector /// vec4 s = sample(image, samplerCoord(image)); vec2 vector = s.rg - 0.5; float len = length(vector); float H = atan(vector.y,vector.x); // convert hue to a RGB color H *= 3.0/3.1415926; // now range [3,3) float i = floor(H); float f = H-i; float a = f; float d = 1.0 - a; vec4 c; if (H<-3.0) c = vec4(0, 1, 1, 1); else if (H<-2.0) c = vec4(0, d, 1, 1); else if (H<-1.0) c = vec4(a, 0, 1, 1); else if (H<0.0) c = vec4(1, 0, d, 1); else if (H<1.0) c = vec4(1, a, 0, 1); else if (H<2.0) c = vec4(d, 1, 0, 1); else if (H<3.0) c = vec4(0, 1, a, 1); else c = vec4(0, 1, 1, 1); // make the color darker if the .xy vector is shorter c.rgb *= clamp((len-minLen)/(maxLen-minLen), 0.0,1.0); /// Add arrow shapes based on the angle from the .xy vector /// float tipAngleRadians = tipAngle * 3.1415/180.0; vec2 dc = destCoord(); // current coordinate vec2 dcm = floor((dc/size)+0.5)*size; // cell center coordinate vec2 delta = dcm - dc; // coordinate relative to center of cell // sample the .xy vector from the center of each cell vec4 sm = sample(image, samplerTransform(image, dcm)); vector = sm.rg - 0.5; len = length(vector); H = atan(vector.y,vector.x); float rotx, k, sideOffset, sideAngle; // these are the three sides of the arrow rotx = delta.x*cos(H) - delta.y*sin(H); sideOffset = size*0.5*cos(tipAngleRadians); k = 1.0 - clamp(rotx-sideOffset, 0.0, 1.0); c.rgb *= k; sideAngle = (3.14159 - tipAngleRadians)/2.0; sideOffset = 0.5 * sin(tipAngleRadians / 2.0); rotx = delta.x*cos(H-sideAngle) - delta.y*sin(H-sideAngle); k = clamp(rotx+size*sideOffset, 0.0, 1.0); c.rgb *= k; rotx = delta.x*cos(H+sideAngle) - delta.y*sin(H+sideAngle); k = clamp(rotx+ size*sideOffset, 0.0, 1.0); c.rgb *= k; /// return the color premultiplied c *= s.a; return c; }
-
23:26 - Optical Flow Visualizer (CIFilter code)
class OpticalFlowVisualizerFilter: CIFilter { var inputImage: CIImage? let callback: CIKernelROICallback = { (index, rect) in return rect } static var kernel: CIKernel = { () -> CIKernel in let url = Bundle.main.url(forResource: "OpticalFlowVisualizer", withExtension: "ci.metallib")! let data = try! Data(contentsOf: url) return try! CIKernel(functionName: "flowView2", fromMetalLibraryData: data) }() override var outputImage : CIImage? { get { guard let input = inputImage else {return nil} return OpticalFlowVisualizerFilter.kernel.apply(extent: input.extent, roiCallback: callback, arguments: [input, 0.0, 100.0, 10.0, 30.0]) } } }
-
23:42 - Optical Flow Visualizer (Vision code)
var requestHandler = VNSequenceRequestHandler() var previousImage:CIImage? if (self.previousImage == nil) { self.previousImage = request.sourceImage } let visionRequest = VNGenerateOpticalFlowRequest(targetedCIImage: source, options: [:]) do { try self.requestHandler.perform([visionRequest], on: self.previousImage!) if let pixelBufferObservation = visionRequest.results?.first as? VNPixelBufferObservation { source = CIImage(cvImageBuffer: pixelBufferObservation.pixelBuffer) } } catch { print(error) } // store the previous image self.previousImage = request.sourceImage let ciFilter = OpticalFlowVisualizerFilter() ciFilter.inputImage = source let output = ciFilter.outputImage
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。