WWDC Quick Look 💓 By SwiftGGTeam
Explore Computer Vision APIs

Explore Computer Vision APIs

观看原视频

Highlight

Apple 在 iOS 14 中把 Core Image 预处理、Vision 轮廓检测和逐像素 optical flow 接成同一条图像管线,让 App 可以直接在设备端分析图像边缘、打孔卡形状和视频帧运动。

核心内容

很多 App 并不以计算机视觉为主业,却会在某个步骤里需要它。银行 App 可以让用户拍支票,收据 App 可以读取票面信息,扫码功能可以省掉手动输入。用户看到的是少填几行表单,开发者面对的是图像预处理、分析请求、结果可视化和性能成本。

这场 session 没有把 Vision、Core Image 和 Core ML 当成彼此分开的框架介绍。它先把 Core Image 放在 Vision 前后:输入图像可以先缩放、去噪、阈值化、调对比度;Vision 的输出也可以交回 Core Image 或 Core Graphics 画出来。这样做的目标很明确:让图像一直留在系统优化过的管线里,减少矩阵转换和额外内存开销。

主角有两个新增能力。第一个是 Vision 的 contour detection(轮廓检测),它返回 VNContoursObservation 和可遍历的 VNContour 层级,适合找边缘、洞、外轮廓和内轮廓。第二个是 optical flow(光流),它分析两帧之间每个像素的 X/Y 位移,适合看视频画面里局部物体的运动,而不是只估算整张图的整体平移。

session 用一个打孔卡例子把这条路跑通。原始图像直接检测会得到 387 个轮廓。加入 Core Image 的模糊和颜色控制后,结果缩到 32 个轮廓,正好对应要找的孔洞。这个变化比 API 名称更重要:开发者可以用业务知识先清理输入,再让 Vision 做分析。

详细内容

1. 先用 Core Image 改善 Vision 输入(02:29

Core Image 在这里承担两类工作。第一类是预处理:缩放图像、形态学处理、转灰度、降噪、边缘检测、增强对比度、阈值化。session 明确点名了 CILanczosScaleCIMorphologyRectangleMaximumCIMorphologyRectangleMinimumCIColorMatrixCIMedianFilterCIGaborGradientsCIColorThresholdCIColorThresholdOtsu

第二类是后处理。Vision 返回 barcode observation、face observation 或 vector field 之后,Core Image 可以把结果重新画成图像。颜色空间也由 Core Image 处理:输入会进入它的 working space,需要特殊颜色空间时,再用 matchedFromWorkingSpacematchedToWorkingSpace 包住自己的算法。

关键点:

  • CIColorThreshold 让 App 自己设阈值,CIColorThresholdOtsu 根据直方图自动找阈值。
  • CIColorAbsoluteDifferenceCILabDeltaE 用来比较两张图,适合进入后面的运动检测流程。
  • 图像先留在 CIImage 管线里,再交给 Vision,可以避免把图像转成矩阵带来的内存和计算成本。

2. 用 Vision 找轮廓,再把结果画回图像(19:24

轮廓检测的请求是 VNDetectContoursRequest。请求执行后,结果是 VNContoursObservation。它有 topLevelContourscontourCountnormalizedPath。单个 VNContour 还带有 child contours、index path、normalized points、point count、aspect ratio 和 normalized path。

这段官方 playground 代码展示了完整流程:读取打孔卡图像,创建 contour request,用 VNImageRequestHandler 执行 Vision 请求,再把 normalizedPath 叠加回原图。

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")
}

关键点:

  • VNDetectContoursRequest.init() 是本例的 Vision 任务,输入图像来自 CIImage
  • 注释掉的 gaussianBlurcolorControls 是 Core Image 预处理。demo 中打开这段后,轮廓数量从 387 降到 32。
  • VNImageRequestHandler.init(ciImage: inputImage, options: [:]) 让 Vision 直接消费 Core Image 输出。
  • contoursObservation.normalizedPath 是可渲染的 CGPath,代码把它缩放到源图尺寸后画到原图上。
  • Vision 使用归一化坐标。session 用 1920x1080 图中的圆形说明,做几何判断时要考虑原图 aspect ratio。

3. 分析轮廓形状,而不只看边缘像素(12:51

VNContoursObservation 的层级结构适合描述外轮廓和内轮廓。session 里的两个方框和两个圆形例子中,方框是 top-level contours,圆形是 child contours。开发者可以用 index path 遍历这棵轮廓图。

轮廓检测的结果不是一堆像素点。VNContour 提供 normalized points 和 point count,描述的是线段路径。Vision 还提供 VNGeometryUtils,可以计算 bounding circle、area 和 perimeter。需要把拍摄抖动造成的小折线去掉时,可以用 polygon approximation 的 epsilon,把带噪声的矩形简化成四个角点。

关键点:

  • maximumImageDimension 是性能和精度的旋钮。低分辨率运行更快,高分辨率能让轮廓更贴近边缘。
  • aspect ratio 会影响几何判断。归一化坐标里,高度为 1.0 的圆,在 1920x1080 图像中宽度会变成 0.5625。
  • epsilon 适合把真实世界里略微弯折的边界压成主要角点,后续才能判断四边形、圆形或孔洞。

4. 用 optical flow 看两帧之间的局部运动(21:23

传统 image registration 给的是整张图的对齐。摄像头整体上移时,它能告诉你图像整体移动了多少。optical flow 更细,它返回每个像素的 X/Y 位移。两个点彼此分开时,整图配准很难表达这种变化,光流可以保留局部运动。

Vision 的结果是 VNPixelBufferObservation,其中包含浮点图像和交错的 X/Y movement。这个结果适合后续算法处理,直接看数值很难理解。session 因此用 Core Image 自定义 filter 把位移转成颜色和箭头。

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

关键点:

  • VNSequenceRequestHandler 用于跨帧请求,因为 optical flow 要比较当前帧和上一帧。
  • VNGenerateOpticalFlowRequest(targetedCIImage: source, options: [:]) 生成光流请求。
  • perform([visionRequest], on: self.previousImage!) 把上一帧交给请求处理。
  • VNPixelBufferObservation 的 pixel buffer 被包装成 CIImage,继续进入 Core Image 可视化步骤。
  • self.previousImage = request.sourceImage 保存当前帧,为下一次请求准备参照图像。

5. 用 Core Image filter 可视化光流(23:26

Vision 给出的光流值偏向算法输入。为了调试和展示,session 加了一个 OpticalFlowVisualizerFilter。它从 App bundle 里加载 OpticalFlowVisualizer.ci.metallib,调用名为 flowView2 的 kernel,把 X/Y movement 映射为颜色深浅和方向箭头。

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])
		}
	}
}

关键点:

  • Bundle.main.url(forResource:withExtension:) 加载 slide attachments 中提供的 Core Image kernel 产物。
  • CIKernel(functionName: "flowView2", fromMetalLibraryData: data) 绑定自定义 kernel。
  • kernel.apply 的参数包括输入图像、最小/最大长度、箭头单元大小和箭头角度。
  • 这一步说明 Core Image 既能在 Vision 前清理输入,也能在 Vision 后把结果变成用户或开发者能理解的图像。

核心启发

  • 纸质工单孔位读取:做什么:让 App 拍摄打孔卡、设备标签或固定格式工单,并把孔洞位置转成结构化记录。为什么值得做:session 的打孔卡 demo 证明,Core Image 预处理能把轮廓数量从 387 收敛到 32。怎么开始:把照片转成 CIImage,先用 gaussianBlurcolorControls 清理背景,再执行 VNDetectContoursRequest,用 contourCountnormalizedPath 检查结果。
  • 视频运动方向叠加层:做什么:在运动训练或视频剪辑 App 里显示画面局部的移动方向。为什么值得做:VNGenerateOpticalFlowRequest 返回逐像素 X/Y 位移,能表达局部物体分开的运动。怎么开始:用 VNSequenceRequestHandler 保存上一帧,把 VNPixelBufferObservation 包成 CIImage,再接入自定义 Core Image filter。
  • 图像分析前的质量检查面板:做什么:给内部工具加一个预览面板,显示阈值化、降噪、轮廓数量和最终叠加图。为什么值得做:session 反复强调预处理会影响 Vision 的速度和稳定性。怎么开始:把 CIColorThresholdOtsuCIMedianFilterCIColorControls 作为可调步骤,旁边显示 VNContoursObservation.contourCount
  • 形状缺陷检测:做什么:检查包装、票据边缘或零件轮廓是否偏离预期形状。为什么值得做:VNGeometryUtils 能计算 area、perimeter 和 bounding circle,epsilon polygon approximation 能把噪声边缘简化成主要角点。怎么开始:先检测轮廓,再按 aspect ratio、point count 和面积范围筛选异常形状。
  • Vision 结果调试视图:做什么:给相机类 App 增加开发者模式,把 Vision 输出直接画回画面。为什么值得做:session 的 drawContours 和 optical flow visualizer 都用可视化缩短调参反馈。怎么开始:轮廓走 normalizedPath 叠加,光流走 VNPixelBufferObservation 到 Core Image filter。

关联 Session

评论

GitHub Issues · utterances