上一篇我們用 Rust + OpenCV 的 YuNet 模型做了靜態圖片和 Webcam 的即時人臉偵測。偵測到人臉之後,下一步自然會想到:能不能自動把人臉打上馬賽克?

這就是 face-mosaic 這個專案要做的事——讀入一段影片,自動偵測每一幀裡的人臉,打上模糊或像素化效果,然後輸出一支全新的影片。整個過程完全自動化,不需要手動框選任何東西。

從 face-detect 到 face-mosaic

如果你看過前一篇文章,會發現 face-detect 已經解決了最核心的問題:用 YuNet 找到人臉的 Bounding Box。face-mosaic 要做的就是在這個基礎上加兩件事:

  1. 影片處理管線:逐幀讀取 → 偵測 → 打碼 → 寫入輸出影片
  2. 模糊/像素化效果:在偵測到的人臉區域上套用視覺遮蔽

聽起來簡單,但實際動手才會發現影片處理有很多細節要處理。

影片處理的基本架構

影片處理的核心是一個 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 幀的迴圈裡更有信心。

參考資源