上一篇我們用 Rust + OpenCV 的 YuNet 模型做了靜態圖片和 Webcam 的即時人臉偵測。偵測到人臉之後,下一步自然會想到:能不能自動把人臉打上馬賽克?
這就是 face-mosaic 這個專案要做的事——讀入一段影片,自動偵測每一幀裡的人臉,打上模糊或像素化效果,然後輸出一支全新的影片。整個過程完全自動化,不需要手動框選任何東西。
從 face-detect 到 face-mosaic
如果你看過前一篇文章,會發現 face-detect 已經解決了最核心的問題:用 YuNet 找到人臉的 Bounding Box。face-mosaic 要做的就是在這個基礎上加兩件事:
- 影片處理管線:逐幀讀取 → 偵測 → 打碼 → 寫入輸出影片
- 模糊/像素化效果:在偵測到的人臉區域上套用視覺遮蔽
聽起來簡單,但實際動手才會發現影片處理有很多細節要處理。
影片處理的基本架構
影片處理的核心是一個 read-process-write 的迴圈。OpenCV 的 VideoCapture 負責讀取,VideoWriter 負責輸出:
let mut cap = videoio::VideoCapture::from_file(&input_path, videoio::CAP_ANY)?;
let fps = cap.get(videoio::CAP_PROP_FPS)?;
let width = cap.get(videoio::CAP_PROP_FRAME_WIDTH)? as i32;
let height = cap.get(videoio::CAP_PROP_FRAME_HEIGHT)? as i32;
let total_frames = cap.get(videoio::CAP_PROP_FRAME_COUNT)? as i64;
let mut writer = videoio::VideoWriter::new(
&output_path,
videoio::VideoWriter::fourcc('m', 'p', '4', 'v')?,
fps,
Size::new(width, height),
true, // isColor
)?;這裡有幾個重要參數:
- FPS:從原始影片直接讀取,確保輸出影片的播放速度跟原始影片一致
- fourcc:影片編碼格式。
mp4v是 MPEG-4 編碼,跟.mp4容器相容 - 尺寸:直接沿用原始影片的解析度
逐幀偵測與打碼
主要的處理迴圈長這樣:
let mut frame = Mat::default();
let mut frame_count: i64 = 0;
loop {
cap.read(&mut frame)?;
if frame.empty() {
break;
}
frame_count += 1;
// 更新偵測器的輸入尺寸
detector.set_input_size(frame.size()?)?;
// 偵測人臉
let mut faces = Mat::default();
detector.detect(&frame, &mut faces)?;
// 對每張偵測到的臉打碼
for i in 0..faces.rows() {
let x = *faces.at_2d::<f32>(i, 0)? as i32;
let y = *faces.at_2d::<f32>(i, 1)? as i32;
let w = *faces.at_2d::<f32>(i, 2)? as i32;
let h = *faces.at_2d::<f32>(i, 3)? as i32;
apply_mosaic(&mut frame, Rect::new(x, y, w, h))?;
}
writer.write(&frame)?;
if frame_count % 100 == 0 {
println!("Processed {}/{} frames", frame_count, total_frames);
}
}每 100 幀印一次進度,因為處理長影片時沒有任何回饋會讓人以為程式當掉了。
馬賽克效果的兩種實現
臉部打碼可以用兩種方式實現:高斯模糊和像素化(馬賽克)。
高斯模糊
最直觀的做法是對人臉區域套用高斯模糊。OpenCV 的 gaussian_blur 一行搞定:
fn apply_blur(frame: &mut Mat, roi: Rect) -> Result<()> {
// 確保 ROI 不會超出圖片邊界
let roi = clamp_rect(roi, frame.size()?);
let mut face_region = Mat::roi(frame, roi)?;
let mut blurred = Mat::default();
// kernel size 越大越模糊,必須是奇數
imgproc::gaussian_blur(
&face_region,
&mut blurred,
Size::new(99, 99),
30.0, // sigmaX
30.0, // sigmaY
opencv::core::BORDER_DEFAULT,
)?;
blurred.copy_to(&mut face_region)?;
Ok(())
}像素化(馬賽克)
像素化的原理也很巧妙:先把人臉區域縮小到很小(比如 10×10),再放大回原本的尺寸。因為放大時用的是最近鄰插值(Nearest Neighbor),就會產生經典的馬賽克方塊效果:
fn apply_mosaic(frame: &mut Mat, roi: Rect) -> Result<()> {
let roi = clamp_rect(roi, frame.size()?);
let face_region = Mat::roi(frame, roi)?;
let pixel_size = 10;
let small_size = Size::new(
(roi.width / pixel_size).max(1),
(roi.height / pixel_size).max(1),
);
// 縮小
let mut small = Mat::default();
imgproc::resize(
&face_region, &mut small,
small_size, 0.0, 0.0,
imgproc::INTER_LINEAR,
)?;
// 放大回原尺寸(最近鄰插值 = 馬賽克效果)
let mut mosaic = Mat::default();
imgproc::resize(
&small, &mut mosaic,
Size::new(roi.width, roi.height), 0.0, 0.0,
imgproc::INTER_NEAREST,
)?;
let mut face_mut = Mat::roi_mut(frame, roi)?;
mosaic.copy_to(&mut face_mut)?;
Ok(())
}這種「先縮後放」的技巧比逐像素手動計算簡單很多,而且完全利用了 OpenCV 既有的 resize 函式。
ROI 邊界處理:一個容易被忽略的坑
YuNet 回傳的 Bounding Box 座標有時候會超出圖片邊界,特別是當人臉在畫面邊緣的時候。如果不處理,OpenCV 會直接 panic。所以需要一個 clamp 函式:
fn clamp_rect(rect: Rect, size: Size) -> Rect {
let x = rect.x.max(0);
let y = rect.y.max(0);
let w = rect.width.min(size.width - x);
let h = rect.height.min(size.height - y);
Rect::new(x, y, w.max(0), h.max(0))
}這個函式確保 ROI 永遠在圖片範圍內。看起來很小,但沒有它,處理真實世界的影片時幾乎一定會爆掉。
效能考量
處理影片跟處理靜態圖片最大的差異在於效能。一段 30fps、10 分鐘的影片有 18,000 幀,每一幀都要跑一次 YuNet 偵測。幾個值得注意的點:
- 不需要對每幀做縮放:影片的每一幀解析度是固定的,不像靜態圖片可能有各種解析度。
set_input_size只需要在第一幀或解析度改變時呼叫 - YuNet 夠快:這個模型是為邊緣裝置設計的,在一般筆電上一幀只需要幾毫秒
- 瓶頸在 I/O:影片的讀寫比偵測本身更花時間,特別是寫入大型 MP4 檔案時
在我的測試中,處理一段 1080p 影片大約能跑到 15-25 fps,主要受限於影片解碼和編碼的速度。
跟 face-detect 的程式碼共用
face-mosaic 和 face-detect 的核心偵測邏輯完全一樣——都是用 FaceDetectorYN 呼叫 YuNet 模型。差別在於:
| face-detect | face-mosaic | |
|---|---|---|
| 輸入 | 靜態圖片 / Webcam | 影片檔案 |
| 輸出 | 畫框 + 特徵點 | 模糊/馬賽克 |
| 處理方式 | 單張 / 即時串流 | 逐幀批次處理 |
| 寫入 | 圖片 / GUI 視窗 | VideoWriter → MP4 |
兩個專案都用了本地 patch 過的 opencv-rust(因為要支援 OpenCV 5),依賴結構也幾乎相同。
學到的東西
這個專案最有趣的不是演算法,而是影片處理的工程細節:
| 概念 | 應用 |
|---|---|
VideoCapture / VideoWriter |
OpenCV 的影片 I/O API |
| fourcc 編碼 | 指定影片壓縮格式 |
| ROI (Region of Interest) | 只對圖片的局部區域操作 |
| Nearest Neighbor 插值 | 放大時產生馬賽克效果 |
| 邊界 Clamp | 防止 ROI 超出圖片範圍 |
結語
face-mosaic 是 face-detect 的自然延伸。人臉偵測本身只是起點,真正有趣的是你拿偵測結果來做什麼——這裡是打碼,但同樣的架構也可以拿來做人臉追蹤、表情辨識、或者臉部特效。
影片處理讓整個專案的複雜度跳了一級,但 Rust 的型別系統在這裡依然很有幫助:Mat 的生命週期管理、Result 的錯誤傳播、以及編譯器對邊界條件的提醒,都讓你在處理 18,000 幀的迴圈裡更有信心。
參考資源
- face-mosaic 原始碼 — 本文範例的完整程式碼
- face-detect 原始碼 — 人臉偵測基礎專案
- OpenCV Zoo — YuNet 模型
- opencv-rust — Rust 的 OpenCV 綁定庫