iOS Metal 圖像顏色蒙版渲染器

ColorOverlayRenderer

基于 Metal 的 iOS 圖像顏色蒙版渲染器,支持一次性渲染和實(shí)時(shí)預(yù)覽兩種模式。核心能力是為任意 PNG/手繪圖像自動(dòng)生成閉合區(qū)域的彩色背景蒙版。

功能概覽

功能 方法 說明
一次性渲染 applyOverlay(to:color:expandRadius:) 輸入 UIImage + 顏色,輸出帶蒙版的 UIImage
預(yù)處理 prepareForRealtimeRendering(image:expandRadius:) 緩存 CPU 泛洪 + GPU 膨脹結(jié)果,為實(shí)時(shí)預(yù)覽做準(zhǔn)備
實(shí)時(shí)預(yù)覽 renderToView() 僅執(zhí)行顏色疊加 Pass,渲染到 MTKView
導(dǎo)出 exportCurrentResult() 將當(dāng)前預(yù)覽結(jié)果編碼為 UIImage
清理 cleanupRealtimeCache() 釋放緩存紋理
效果預(yù)覽

)

架構(gòu)

┌─────────────────────────────────────────────────────────┐
│                   ColorOverlayRenderer                   │
│                                                          │
│  ┌──────────────────────────────────────────────────┐   │
│  │          一次性模式 (applyOverlay)                 │   │
│  │  UIImage → CPU泛洪 → Pass1 → Pass2 → UIImage     │   │
│  └──────────────────────────────────────────────────┘   │
│                                                          │
│  ┌──────────────────────────────────────────────────┐   │
│  │          實(shí)時(shí)預(yù)覽模式                              │   │
│  │  prepareForRealtimeRendering:                     │   │
│  │    UIImage → CPU泛洪 → Pass1 → 緩存紋理           │   │
│  │                                                    │   │
│  │  renderToView (每次顏色變化):                      │   │
│  │    緩存紋理 → Pass2 → Render Pass → MTKView       │   │
│  │                                                    │   │
│  │  exportCurrentResult:                              │   │
│  │    緩存紋理 → Pass2 → 像素回讀 → UIImage           │   │
│  └──────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

Metal Pipeline 總覽

渲染器初始化時(shí)編譯 3 條 Pipeline:

Pipeline 類型 Shader 函數(shù) 用途
dilatePipelineState Compute dilate_mask Pass 1: 將基礎(chǔ) Mask 按半徑做圓形膨脹
applyOverlayPipelineState Compute apply_color_overlay Pass 2: 根據(jù)膨脹 Mask 和顏色生成最終蒙版圖像
renderPipelineState Render quad_vertex_main + quad_fragment_main 將 Compute 輸出紋理 Aspect-Fit 繪制到 MTKView

功能一:一次性渲染 (applyOverlay)

完整的同步渲染流程,輸入 UIImage + 顏色,輸出帶蒙版的 UIImage。適用于不需要實(shí)時(shí)預(yù)覽的場景。

流程

UIImage
  │
  ▼
┌─────────────────────────────┐
│ 1. 創(chuàng)建 Padded 畫布          │  原圖四周各加 expandRadius 像素的透明邊距
│    (width + 2*radius) ×      │  確保膨脹后的 Mask 不會(huì)被畫布邊界截?cái)?│    (height + 2*radius)       │
└──────────────┬──────────────┘
               │
               ▼
┌─────────────────────────────┐
│ 2. CPU 泛洪生成基礎(chǔ) Mask     │  GraphicAlgorithm.generateSolidMask()
│    (詳見下方 "背景蒙版生成")  │  輸出: 單通道 [UInt8] (0=外部, 255=內(nèi)部)
└──────────────┬──────────────┘
               │
               ▼
┌─────────────────────────────┐
│ 3. 創(chuàng)建 Metal 紋理           │  inTexture (rgba8Unorm): 原始圖像像素
│                              │  cpuMaskTexture (r8Unorm): CPU 生成的基礎(chǔ) Mask
│                              │  dilatedMaskTexture (r8Unorm): 膨脹后的 Mask
│                              │  outTexture (rgba8Unorm): 最終輸出
└──────────────┬──────────────┘
               │
               ▼
┌─────────────────────────────┐
│ 4. GPU Pass 1: dilate_mask  │  Compute Shader
│                              │  輸入: cpuMaskTexture + expandRadius
│                              │  輸出: dilatedMaskTexture
│                              │  原理: 對每個(gè)像素搜索半徑內(nèi)是否存在 Mask 有效像素
│                              │        使用圓形內(nèi)核 (i2+j2 ≤ r2) 確保邊緣圓滑
└──────────────┬──────────────┘
               │
               ▼
┌─────────────────────────────┐
│ 5. GPU Pass 2:              │  Compute Shader
│    apply_color_overlay       │  輸入: inTexture + dilatedMaskTexture + 顏色參數(shù)
│                              │  輸出: outTexture
│                              │  邏輯:
│                              │    mask > 0.5 且 原始alpha > 0 → 保留原始像素(前景)
│                              │    mask > 0.5 且 原始alpha = 0 → 填充指定顏色(背景)
│                              │    mask ≤ 0.5 → 全透明(外部區(qū)域)
└──────────────┬──────────────┘
               │
               ▼
┌─────────────────────────────┐
│ 6. 像素回讀                  │  outTexture.getBytes() → CGContext → CGImage → UIImage
│                              │  保留原始 image.scale 和 image.imageOrientation
└─────────────────────────────┘

功能二:實(shí)時(shí)預(yù)覽模式

將渲染流程拆分為"預(yù)處理"和"實(shí)時(shí)渲染"兩個(gè)階段。預(yù)處理只執(zhí)行一次(CPU 泛洪 + Pass 1),后續(xù)顏色變化時(shí)僅重新執(zhí)行 Pass 2,實(shí)現(xiàn)毫秒級響應(yīng)。

階段 1: 預(yù)處理 (prepareForRealtimeRendering)

UIImage
  │
  ▼
┌──────────────────────────────────┐
│ CPU 泛洪 + GPU Pass 1 (同上)      │
│                                   │
│ 緩存結(jié)果:                         │
│   cachedInTexture ← inTexture     │
│   cachedDilatedMaskTexture ← dilatedMaskTexture
│   cachedOutTexture ← outTexture   │
│   cachedImageScale ← image.scale  │
│   cachedImageOrientation          │
│   isPrepared = true               │
└──────────────────────────────────┘

階段 2: 實(shí)時(shí)渲染 (renderToView)

每次 overlayColor 變化時(shí)觸發(fā):

overlayColor didSet
  │
  ▼
mtkView.setNeedsDisplay()
  │
  ▼
MTKViewDelegate.draw(in:)
  │
  ▼
renderToView()
  │
  ├─── Compute Pass ──────────────────────────────────┐
  │    apply_color_overlay:                            │
  │    cachedInTexture + cachedDilatedMaskTexture      │
  │    → cachedOutTexture                              │
  │    (僅執(zhí)行 Pass 2,跳過 CPU 泛洪和 Pass 1)         │
  └────────────────────────────────────────────────────┘
  │
  ├─── Render Pass ───────────────────────────────────┐
  │    quad_vertex_main + quad_fragment_main:           │
  │    將 cachedOutTexture 以 Aspect-Fit 方式           │
  │    居中繪制到 drawable.texture                      │
  │                                                     │
  │    NDC 坐標(biāo)計(jì)算:                                    │
  │    scaleFit = min(viewW/imgW, viewH/imgH, 1.0)     │
  │    ndcScaleX = (imgW * scaleFit) / viewW            │
  │    ndcScaleY = (imgH * scaleFit) / viewH            │
  │    頂點(diǎn)范圍: [-ndcScaleX, -ndcScaleY]               │
  │           到 [+ndcScaleX, +ndcScaleY]               │
  └─────────────────────────────────────────────────────┘
  │
  ▼
commandBuffer.present(drawable) + commit()

階段 3: 導(dǎo)出 (exportCurrentResult)

用戶確認(rèn)保存時(shí)調(diào)用,同步執(zhí)行 Pass 2 并回讀像素:

exportCurrentResult()
  │
  ▼
Compute Pass: apply_color_overlay → cachedOutTexture
  │
  ▼
waitUntilCompleted()  ← 同步等待 GPU 完成
  │
  ▼
outTexture.getBytes() → CGContext → CGImage → UIImage(scale, orientation)

背景蒙版 CPU 端生成原理

GraphicAlgorithm.generateSolidMask() 負(fù)責(zé)從圖像像素?cái)?shù)據(jù)中識(shí)別"閉合區(qū)域",生成單通道 Mask。

核心思路

將圖像視為一個(gè)二維網(wǎng)格,非透明像素構(gòu)成"墻壁",透明像素構(gòu)成"通道"。從圖像邊緣開始向內(nèi)泛洪,所有能從邊緣到達(dá)的透明像素都是"外部背景",無法到達(dá)的透明像素則被"墻壁"包圍,屬于"內(nèi)部區(qū)域"。

算法流程

┌─────────────────────────────────────────────────────┐
│ 階段 1: 統(tǒng)計(jì)筆觸面積                                 │
│                                                      │
│ 遍歷所有像素,統(tǒng)計(jì) alpha > 0 的像素?cái)?shù)量 (strokeArea)   │
│ 每隔 5 個(gè)像素采樣坐標(biāo),用于后續(xù)凸包兜底                │
└──────────────────────┬──────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────────────┐
│ 階段 2: BFS 泛洪填充                                 │
│                                                      │
│ 1. 初始化: memset(maskData, 255, totalPixels)        │
│    假設(shè)所有像素都是"內(nèi)部有效區(qū)域"                      │
│                                                      │
│ 2. 種子注入: 掃描圖像四周邊緣                         │
│    如果邊緣像素是透明的 (alpha == 0):                 │
│      maskData[index] = 0  (標(biāo)記為外部)                │
│      加入 BFS 隊(duì)列                                    │
│                                                      │
│ 3. BFS 擴(kuò)散: 從種子開始,4 方向遍歷                   │
│    對每個(gè)鄰居: 如果 maskData == 255 且 alpha == 0     │
│      → 標(biāo)記為外部 (maskData = 0),加入隊(duì)列            │
│    非透明像素 (alpha > 0) 會(huì)阻斷泛洪傳播              │
│                                                      │
│ 結(jié)果: 被非透明像素包圍的透明區(qū)域保持 255 (內(nèi)部)        │
│       與邊緣連通的透明區(qū)域變?yōu)?0 (外部)                │
└──────────────────────┬──────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────────────┐
│ 階段 3: 校驗(yàn) + 凸包兜底                              │
│                                                      │
│ 計(jì)算 fillRatio = maskArea / strokeArea               │
│                                                      │
│ 如果 fillRatio < 1.05 (填充面積幾乎沒有增加):         │
│   說明泛洪失敗 (圖形未形成閉合區(qū)域)                    │
│   觸發(fā)凸包兜底策略:                                   │
│     1. 對采樣點(diǎn)計(jì)算 Monotone Chain 凸包               │
│     2. 用 CoreGraphics 將凸包多邊形光柵化到 maskData  │
│     3. 凸包內(nèi)部 = 255, 外部 = 0                      │
└─────────────────────────────────────────────────────┘

BFS 泛洪示意

原始圖像 (. = 透明, # = 非透明):     Mask 結(jié)果 (0 = 外部, 1 = 內(nèi)部):

. . . . . . . .                      0 0 0 0 0 0 0 0
. . # # # # . .                      0 0 1 1 1 1 0 0
. # . . . . # .                      0 1 1 1 1 1 1 0
. # . . . . # .          →           0 1 1 1 1 1 1 0
. # . . . . # .                      0 1 1 1 1 1 1 0
. . # # # # . .                      0 0 1 1 1 1 0 0
. . . . . . . .                      0 0 0 0 0 0 0 0

邊緣透明像素從四周向內(nèi)泛洪,被 # 圍住的內(nèi)部透明像素?zé)o法被到達(dá),保持為 1。

性能優(yōu)化

  • 使用 UnsafeMutablePointer<FloodPoint> 分配連續(xù)內(nèi)存作為 BFS 隊(duì)列,避免 Swift Array 的動(dòng)態(tài)擴(kuò)容開銷
  • FloodPoint 使用 Int16 存儲(chǔ)坐標(biāo),節(jié)省內(nèi)存(支持最大 32767×32767 圖像)
  • memset 初始化 Mask,比逐元素賦值快一個(gè)數(shù)量級
  • @inline(__always) 標(biāo)記入隊(duì)輔助函數(shù),消除函數(shù)調(diào)用開銷

GPU Shader 詳解

Pass 1: dilate_mask (Compute)

將基礎(chǔ) Mask 按指定半徑做圓形形態(tài)學(xué)膨脹。

輸入:
  texture(0): cpuMaskTexture (r8Unorm) - CPU 生成的基礎(chǔ) Mask
  buffer(0):  radius (int) - 膨脹半徑

輸出:
  texture(1): dilatedMaskTexture (r8Unorm) - 膨脹后的 Mask

算法:
  對每個(gè)像素,在半徑 r 的圓形區(qū)域內(nèi)搜索:
    如果找到任何 mask > 0.5 的鄰居 → 輸出 1.0
    否則 → 輸出 0.0

  圓形判定: i2 + j2 ≤ r2
  提前退出: 找到有效鄰居后立即 break,避免無效搜索

Pass 2: apply_color_overlay (Compute)

根據(jù)膨脹后的 Mask 和指定顏色生成最終蒙版圖像。

輸入:
  texture(0): inTexture (rgba8Unorm) - 原始圖像
  texture(1): maskTexture (r8Unorm) - 膨脹后的 Mask
  buffer(0):  OverlayColor { float4 color } - 疊加顏色 RGBA

輸出:
  texture(2): outTexture (rgba8Unorm) - 最終結(jié)果

邏輯:
  if mask > 0.5:
    if 原始 alpha > 0:  → 保留原始像素 (前景筆畫)
    else:               → 填充指定顏色 (背景蒙版)
  else:
    → 全透明 (0,0,0,0) (外部區(qū)域)

Render Pass: quad_vertex_main + quad_fragment_main

將 Compute Shader 的輸出紋理以 Aspect-Fit 方式繪制到 MTKView 的 drawable 上。

Vertex Shader:
  輸入: 4 個(gè)頂點(diǎn)坐標(biāo) (NDC) + 4 個(gè)紋理坐標(biāo)
  輸出: 裁剪空間坐標(biāo) + 插值紋理坐標(biāo)
  繪制方式: Triangle Strip (4 頂點(diǎn) = 1 個(gè)矩形)

Fragment Shader:
  輸入: 插值后的紋理坐標(biāo)
  采樣: bilinear filtering (mag_filter::linear, min_filter::linear)
  輸出: 采樣顏色值

Aspect-Fit 計(jì)算 (Swift 端):
  scaleFit = min(viewW/imgW, viewH/imgH, 1.0)  // 不放大,只縮小
  NDC 范圍 = [-scaleFit*imgW/viewW, +scaleFit*imgW/viewW]
  效果: 圖像居中顯示,保持原始寬高比,周圍透明

線程安全

  • NSLock (realtimeLock) 保護(hù)緩存紋理的讀寫
  • prepareForRealtimeRendering 在鎖內(nèi)完成全部預(yù)處理
  • renderToView 在鎖內(nèi)讀取緩存引用,解鎖后執(zhí)行 GPU 渲染
  • applyOverlay 使用獨(dú)立的局部紋理,不訪問緩存,與實(shí)時(shí)渲染完全隔離

SwiftUI 集成

MTKViewRepresentable 將 MTKView 包裝為 SwiftUI 視圖:

MTKViewRepresentable(renderer: ColorOverlayRenderer.shared)

配置:

  • isPaused = true + enableSetNeedsDisplay = true: 按需繪制模式
  • colorPixelFormat = .rgba8Unorm: 匹配緩存紋理格式
  • framebufferOnly = false: 支持像素回讀
  • isOpaque = false: 支持透明背景
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 原創(chuàng):知識(shí)探索型文章創(chuàng)作不易,請珍惜,之后會(huì)持續(xù)更新,不斷完善個(gè)人比較喜歡做筆記和寫總結(jié),畢竟好記性不如爛筆頭哈哈...
    時(shí)光啊混蛋_97boy閱讀 1,706評論 0 3
  • 從數(shù)據(jù)源說起 videoToolBox解碼出來的是CPU與GPU共享內(nèi)存的CVPixelBufferRef格式,渲...
    野碼道人閱讀 3,611評論 0 12
  • 本案例主要是利用Metal實(shí)現(xiàn)攝像頭采集內(nèi)容的即刻渲染處理,理解視頻采集、處理及渲染的流程 視頻實(shí)時(shí)采集并渲染的效...
    含笑州閱讀 1,158評論 0 1
  • 一、屏幕顯像原理 上圖顯示的是CRT電子槍掃描路徑,涉及到兩個(gè)比較重要的概念:水平同步信號(HSync),垂直同步...
    綠葉竹林閱讀 1,259評論 1 3
  • 一、iOS渲染架構(gòu) 下圖分別是iOS渲染早期架構(gòu)和最新架構(gòu),可以看到在最新的架構(gòu)中使用了Metal代替OpenGL...
    Jason1226閱讀 1,612評論 0 3

友情鏈接更多精彩內(nèi)容