Duo 主体识别,Mac 版如何做
iPhone 上的 Duo 主体分离效果惊艳,但 macOS 没有原生入口。sumimakito/Mac-Duo 用 Apple Vision 框架自建一套,从原理解析到可运行代码全程拆解。
源仓库: sumimakito/Mac-Duo
项目是什么,为什么突然火
Duo 是 Apple 在 iOS 16 引入的「主体分离」能力:照片里的人物或物品被精准抠出,背景自动虚化或替换。整套效果依赖 Vision 框架里的 Saliency 与 Instance Segmentation API,并通过 Metal 实时合成。macOS 一直没有官方入口,想在 MacBook 上给设计稿换背景,只能开虚拟机跑 iOS 模拟器。sumimakito/Mac-Duo 用 SwiftUI + Vision 直接把这条链路搬到了 macOS,过去两周 star 从 200 涨到 865,顺势登上了 GitHub Trending。
问题一:Vision 到底返回了什么
现象:调用 VNGenerateImageSaliencyRequest 后拿到一个看上去像 mask 的灰度图,但拼回去却没有任何效果。
根因:Saliency API 只返回显著性热度(saliency heatmap),并不是二值化的 segmentation mask。它告诉你「这里值得关注」,但不告诉你「前景到此为止」。
解法:使用基于注意力的 VNGenerateAttentionBasedSaliencyImageRequest,拿到带 alpha 的 CVPixelBuffer,再交给 CIBlendWithMask 做合成。
import Vision
import CoreImage
func saliencyMask(for image: CIImage) -> CIImage? {
let request = VNGenerateAttentionBasedSaliencyImageRequest()
let handler = VNImageRequestHandler(ciImage: image)
try? handler.perform([request])
guard let obs = request.results?.first else { return nil }
return CIImage(cvPixelBuffer: obs.pixelBuffer)
}
问题二:抠图后边缘总有白边
现象:合成结果在主体轮廓外有 1-2 像素的灰白带。
根因:Saliency 的 alpha 是 0-255 全灰阶,不是硬边的二值 mask,边缘抗锯齿直接被当成了半透明背景。
解法:在喂给 CIBlendWithMask 前,对 mask 做一次轻度高斯模糊并提对比度。
let refined = mask
.applyingFilter("CIGaussianBlur", parameters: [kCIInputRadiusKey: 1.5])
.applyingFilter("CIColorControls", parameters: [
kCIInputContrastKey: 1.2,
kCIInputBrightnessKey: 0.0
])
问题三:拖入视频或 Live Photo 就崩溃
现象:静态图一切正常,把视频拖进窗口直接闪退或卡死。
根因:Saliency 只接受单帧 CIImage,而视频帧需要从 AVPlayerItemVideoOutput 抽帧,buffer 生命周期与 Vision 期望不一致。
解法:在播放循环里同步抽帧,提交后立即让 Vision 内部 copy 一份,再释放抽帧 buffer。
let output = AVPlayerItemVideoOutput(pixelBufferAttributes: [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA
])
player.currentItem?.add(output)
func tick() {
let t = player.currentTime()
guard output.hasNewPixelBuffer(forItemTime: t),
let buf = output.copyPixelBuffer(forItemTime: t,
itemTimeForDisplay: nil)
else { return }
let frame = CIImage(cvPixelBuffer: buf)
if let mask = saliencyMask(for: frame) {
composite(frame, mask: mask)
}
}
问题四:Apple Silicon 上跑得很慢
现象:M1 Pro 处理一张 4K 图要 1.8 秒,UI 明显掉帧。
根因:CIContext 默认走 CPU,软渲染 4K 完全喂不满 GPU。
解法:用 MTLCreateSystemDefaultDevice 构造 Metal 后端的 CIContext,并禁用中间结果缓存。
let device = MTLCreateSystemDefaultDevice()!
let context = CIContext(mtlDevice: device, options: [
.cacheIntermediates: false,
.name: "MacDuoContext"
])
启用后单帧处理时间从 1.8s 降到约 90ms,4K 视频预览也能稳 30fps。
问题五:怎么把它做成一个能 git clone && open 的工程
现象:照着文档拼完代码,发现文件结构混乱、SwiftUI 和 Vision 耦合。
根因:没有把 Vision 调用放到独立 Actor 或 Service,导致主线程被 Vision 阻塞。
解法:参考 Mac-Duo 的目录结构——把 Saliency、合成、渲染拆成三个 actor,主线程只负责 SwiftUI 状态。
actor SaliencyService {
func mask(for image: CIImage) async throws -> CIImage {
try await withCheckedThrowingContinuation { cont in }
}
}
@MainActor
final class ViewModel: ObservableObject {
@Published var preview: CGImage?
func process(_ url: URL) async {
let img = CIImage(contentsOf: url)!
let mask = try? await SaliencyService().mask(for: img)
// 合成 + 渲染回主线程
}
}
这套结构也是 Mac-Duo 在 Issue #12 之后重构的方向,star 增长曲线明显加快。
Sources
本文参考:sumimakito/Mac-Duo GitHub README 与 Issue 区、Apple Vision 官方文档、GitHub Trending 2026-09-15 榜单。