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 明确点名了 CILanczosScale、CIMorphologyRectangleMaximum、CIMorphologyRectangleMinimum、CIColorMatrix、CIMedianFilter、CIGaborGradients、CIColorThreshold 和 CIColorThresholdOtsu。
第二类是后处理。Vision 返回 barcode observation、face observation 或 vector field 之后,Core Image 可以把结果重新画成图像。颜色空间也由 Core Image 处理:输入会进入它的 working space,需要特殊颜色空间时,再用 matchedFromWorkingSpace 和 matchedToWorkingSpace 包住自己的算法。
关键点:
CIColorThreshold让 App 自己设阈值,CIColorThresholdOtsu根据直方图自动找阈值。CIColorAbsoluteDifference和CILabDeltaE用来比较两张图,适合进入后面的运动检测流程。- 图像先留在
CIImage管线里,再交给 Vision,可以避免把图像转成矩阵带来的内存和计算成本。
2. 用 Vision 找轮廓,再把结果画回图像(19:24)
轮廓检测的请求是 VNDetectContoursRequest。请求执行后,结果是 VNContoursObservation。它有 topLevelContours、contourCount 和 normalizedPath。单个 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。- 注释掉的
gaussianBlur和colorControls是 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,先用gaussianBlur和colorControls清理背景,再执行VNDetectContoursRequest,用contourCount和normalizedPath检查结果。 - 视频运动方向叠加层:做什么:在运动训练或视频剪辑 App 里显示画面局部的移动方向。为什么值得做:
VNGenerateOpticalFlowRequest返回逐像素 X/Y 位移,能表达局部物体分开的运动。怎么开始:用VNSequenceRequestHandler保存上一帧,把VNPixelBufferObservation包成CIImage,再接入自定义 Core Image filter。 - 图像分析前的质量检查面板:做什么:给内部工具加一个预览面板,显示阈值化、降噪、轮廓数量和最终叠加图。为什么值得做:session 反复强调预处理会影响 Vision 的速度和稳定性。怎么开始:把
CIColorThresholdOtsu、CIMedianFilter、CIColorControls作为可调步骤,旁边显示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
- Optimize the Core Image pipeline for your video app — 继续看视频场景中的
CIContext复用、Metal command queue 和 Core Image 性能。 - Build Metal-based Core Image kernels with Xcode — 深入本场 optical flow visualizer 背后的
.ci.metalkernel 构建和加载流程。 - Discover Core Image debugging techniques — 学习如何生成 Core Image graph,定位颜色、内存和渲染问题。
- Detect Body and Hand Pose with Vision — 衔接本场提到的 Vision 2020 人体姿态和手部姿态检测能力。
- Explore the Action & Vision app — 看 Vision、Create ML 和 Core ML 如何组合成完整运动分析 App。
评论
GitHub Issues · utterances