iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0
Rust

30天解鎖 Rust 開發者工具箱系列 第 6

「Day 06」Rust 之眼,開!

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250920/20177832cXvquqMbBt.jpg
圖:“Rust 的吉祥物 Ferris the Crab 化身監視器,手拿放大鏡監視都市的點點滴滴”,gemini-2.5-flash-preview,2025年09月16日。

前言:感知世界 -- 電腦視覺

身為 AI 工程師,要讓我們能夠使用 Rust 來進行 AI 應用的開發,讓程式碼能夠感受這個世界是至關重要的。感知世界的第一步 -- 電腦視覺,在 C/C++ 或 Python 生態系進行 AI 應用的開發一定知道 opencv,甚至到了 “因為有 opencv,所以我才放心用這三種語言” 的地步(或主要原因之一)。然而,要我們進入 Rust 的生態系中發展起來,讓我的程式碼能夠看見,這一定是不可逃避的靈魂拷問。

opencv:我來解決你!

實際上,無論是圖片或影像串流幀其實只是一堆巨大的數字矩陣,opencv 提供了各種函數讓我們能夠輕易地操控這些數字矩陣,例如:讀取與寫入、色彩空間轉換、幾何變換、濾波與降噪、特徵提取、圖層疊加等等,這都在影像處理、電腦視覺領域至關重要,而在電腦視覺中已有相當成熟造詣的 AI 應用必定也如此。opencv 讓你的 Rust 宛如裝上一雙強大的眼睛,但眼睛連結到腦的過程是有點不一樣的。

Rust 和 C++ 的共舞

OpenCV 是一個用 C++ 寫成的、擁有數十年歷史的龐大函式庫。而 Rust 中的 opencv 本身並不是用 Rust 從頭重新實作整個 OpenCV,而是靠 FFI (Foreign Function Interface) 的方式銜接 OpenCV 的底層 C++ 演算法。

在使用之前你一定要知道的運作模式:

  • 底層核心:你的電腦上依然需要安裝原生的 OpenCV C++ 函式庫。這才是所有影像處理演算法的真正執行者。
  • Rust Crate:opencv crate 提供了一組符合 Rust 語法風格和安全慣例的 API。
  • FFI 橋樑:當你在 Rust 程式碼中呼叫一個函式,例如 imgproc::canny(...) 時,這個 crate 會透過 FFI 這座橋樑,去呼叫底層 C++ 函式庫中對應的 cv::Canny 函式,並將結果安全地傳回給 Rust。
  • 性能保證:我們依然能享受到原生 OpenCV 經過高度優化、甚至利用硬體加速的演算法性能。
  • 功能完整:龐大的 OpenCV 功能集幾乎都可以被 Rust 使用。
  • Rust 的安全性:opencv crate 的作者們努力地將 C++ 中可能出現的裸指標、手動記憶體管理等不安全操作,封裝在安全的 Rust API 之下。你只需要和 Rust 的所有權(Ownership)、生命週期(Lifetime)打交道,大大降低了犯錯的機率。

所以,當我們在 Rust 中使用 opencv 時,我們其實是站在巨人的肩膀上,用 Rust 的安全韁繩,駕馭著 OpenCV 這匹性能猛獸。

實作範例

專案設定 - Cargo.toml 配置

[dependencies]
opencv = { version = "0.94.4", features = ["highgui", "videoio", "imgproc"] }

今日四個範例:main 函數

範例一:Canny 邊緣檢測

函數原始碼

/// 此函數展示:
/// 1. 使用 imread 從檔案讀取圖像
/// 2. 轉換為灰階圖像
/// 3. 應用高斯模糊以減少雜訊
/// 4. 應用 Canny 邊緣檢測
/// 5. 使用 imwrite 保存結果
fn canny_edge_detection_tutorial() -> Result<(), Box<dyn std::error::Error>> {
    // 設定輸入檔案路徑
    let input_path = "data/test.jpg";

    // 讀取彩色圖像
    let img = imgcodecs::imread(input_path, imgcodecs::IMREAD_COLOR)?;
    // 檢查圖像是否成功讀取
    if img.empty() {
        return Err("無法讀取圖像".into());
    }

    // 將彩色圖像轉換為灰階圖像
    // 這是邊緣檢測的必要步驟,因為 Canny 演算法在灰階圖像上運作
    let mut gray = Mat::default();
    imgproc::cvt_color(&img, &mut gray, imgproc::COLOR_BGR2GRAY, 0, core::AlgorithmHint::ALGO_HINT_DEFAULT)?;

    // 應用高斯模糊來減少圖像雜訊
    // 這有助於減少假邊緣的產生,提高邊緣檢測的準確性
    let mut blurred = Mat::default();
    imgproc::gaussian_blur(&gray, &mut blurred, core::Size::new(5, 5), 0.0, 0.0, core::BORDER_DEFAULT, core::AlgorithmHint::ALGO_HINT_DEFAULT)?;

    // 應用 Canny 邊緣檢測演算法
    // 參數說明:
    // - 50.0: 低閾值,用於檢測強邊緣
    // - 150.0: 高閾值,用於定義連接弱邊緣
    // - 3: Sobel 算子孔徑大小
    // - false: 使用 L1 範數計算梯度幅度(較快)
    let mut edges = Mat::default();
    imgproc::canny(&blurred, &mut edges, 50.0, 150.0, 3, false)?;

    // 保存邊緣檢測結果
    let params = core::Vector::<i32>::new();
    let output_path = "data/test_edges.jpg";
    imgcodecs::imwrite(output_path, &edges, &params)?;

    Ok(())
}

範例二:圖像二值化(Image Binarization)

函數原始碼

/// 此函數展示:
/// 1. 將圖像轉換為灰階
/// 2. 應用二進制閾值處理
/// 3. 保存二值化結果
fn image_binarization_tutorial() -> Result<(), Box<dyn std::error::Error>> {
    // 設定輸入檔案路徑
    let input_path = "data/test.jpg";

    // 讀取彩色圖像
    let img = imgcodecs::imread(input_path, imgcodecs::IMREAD_COLOR)?;
    // 檢查圖像是否成功讀取
    if img.empty() {
        return Err("無法讀取圖像進行二值化".into());
    }

    // 將彩色圖像轉換為灰階圖像
    // 二值化處理需要在灰階圖像上進行
    let mut gray = Mat::default();
    imgproc::cvt_color(&img, &mut gray, imgproc::COLOR_BGR2GRAY, 0, core::AlgorithmHint::ALGO_HINT_DEFAULT)?;

    // 設定閾值參數
    let mut binary = Mat::default();
    let threshold_value = 127.0; // 標準閾值 (0-255 的中間值)
    let max_value = 255.0;       // 二值化圖像的最大值 (白色)

    // 應用二進制閾值處理
    // 像素值 > 閾值 設為 max_value (白色)
    // 像素值 <= 閾值 設為 0 (黑色)
    imgproc::threshold(&gray, &mut binary, threshold_value, max_value, imgproc::THRESH_BINARY)?;
    let binary_output = "data/test_binary.jpg";
    let params = core::Vector::<i32>::new();
    imgcodecs::imwrite(binary_output, &binary, &params)?;

    Ok(())
}

範例三:繪製邊界框與文字

函數原始碼

/// 此函數展示:
/// 1. 繪製一個邊界框
/// 2. 添加文字標籤
/// 3. 保存註解後的圖像
fn bounding_box_text_tutorial() -> Result<(), Box<dyn std::error::Error>> {
    let input_path = "data/test.jpg";

    // 讀取圖像
    let img = imgcodecs::imread(input_path, imgcodecs::IMREAD_COLOR)?;
    if img.empty() {
        return Err("無法讀取圖像進行註解".into());
    }

    // 創建圖像副本進行註解
    let mut annotated_img = img.clone();

    // 繪製一個紅色邊界框
    let bbox = core::Rect::new(100, 100, 200, 150);
    imgproc::rectangle(&mut annotated_img, bbox, Scalar::new(0.0, 0.0, 255.0, 0.0), 3, imgproc::LINE_8, 0)?;

    // 添加文字標籤
    let text = "Object";
    imgproc::put_text(&mut annotated_img, text, core::Point::new(bbox.x, bbox.y - 10),
                      imgproc::FONT_HERSHEY_SIMPLEX, 0.8, Scalar::new(0.0, 0.0, 255.0, 0.0), 2, imgproc::LINE_8, false)?;

    // 保存註解後的圖像
    let annotated_output = "data/test_annotated.jpg";
    let params = core::Vector::<i32>::new();
    imgcodecs::imwrite(annotated_output, &annotated_img, &params)?;

    Ok(())
}

範例四:影片處理與 - 繪製邊緣框與文字

函數原始碼

/// 此函數展示:
/// 1. 開啟並讀取影片檔案
/// 2. 逐幀處理
/// 3. 在影片幀上添加註解
/// 4. 即時顯示與 imshow
/// 5. 鍵盤輸入處理
/// 6. 使用 VideoWriter 保存註解影片
/// 7. 資源清理
fn video_annotation_tutorial() -> Result<(), Box<dyn std::error::Error>> {

    // 設定輸入影片檔案路徑
    let video_path = "data/test_short.mp4";

    // 檢查影片檔案是否存在
    if !Path::new(video_path).exists() {
        return Ok(());
    }

    // 開啟影片檔案
    let mut cap = videoio::VideoCapture::from_file(video_path, videoio::CAP_ANY)?;
    // 檢查影片是否成功開啟
    if !cap.is_opened()? {
        return Err("無法開啟影片檔案".into());
    }

    // 獲取影片屬性
    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 window_name = "Video Annotation Tutorial";
    highgui::named_window(window_name, highgui::WINDOW_AUTOSIZE)?;

    // 初始化 VideoWriter 用於輸出影片
    let output_video_path = "data/annotated_video.mp4";
    // MP4 編碼格式
    let fourcc = videoio::VideoWriter::fourcc('m', 'p', '4', 'v')?;
    // 創建 VideoWriter,使用與輸入影片相同的屬性
    let mut writer = videoio::VideoWriter::new(output_video_path, fourcc, fps, core::Size::new(width, height), true)?;

    // 檢查 VideoWriter 是否成功創建
    if !writer.is_opened()? {
        return Err("無法創建輸出影片檔案".into());
    }

    // 初始化處理變數
    let mut frame = Mat::default();

    // 逐幀處理影片
    while cap.read(&mut frame)? {
        // 檢查是否到達影片結尾
        if frame.empty() {
            break;
        }

        // 為幀添加註解
        let mut annotated_frame = frame.clone();

        // 計算邊界框的中心位置
        let box_width = 200;
        let box_height = 100;
        let center_x = width / 2;
        let center_y = height / 2;
        let box_x = center_x - box_width / 2;
        let box_y = center_y - box_height / 2;

        // 在中心繪製邊界框
        let bbox = core::Rect::new(box_x, box_y, box_width, box_height);
        imgproc::rectangle(&mut annotated_frame, bbox, Scalar::new(0.0, 255.0, 0.0, 0.0), 3, imgproc::LINE_8, 0)?;

        // 在邊界框上方添加文字
        let text = "Center Detection";
        let text_x = box_x;
        let text_y = box_y - 10;
        imgproc::put_text(&mut annotated_frame, text, core::Point::new(text_x, text_y),
                          imgproc::FONT_HERSHEY_DUPLEX, 0.8, Scalar::new(0.0, 255.0, 0.0, 0.0), 2, imgproc::LINE_8, false)?;

        // 將註解後的幀寫入輸出影片
        writer.write(&annotated_frame)?;

        // 顯示註解後的幀
        highgui::imshow(window_name, &annotated_frame)?;

        // 處理鍵盤輸入
        let key = highgui::wait_key(30)?; // 等待 30ms 用於按鍵

        match key {
            113 | 27 => {
                break;
            },
            _ => {}
        }
    }

    // 資源清理
    cap.release()?;
    writer.release()?;
    highgui::destroy_window(window_name)?;

    Ok(())
}

結語:當然不只有 OpenCV

這四個簡單的範例初探了 opencv 已經相當成熟的影像處理流程,讓從 C/C++ 和 Python 生態系過來後,並不會感到陌生,關鍵名詞與邏輯架構也都幾乎沿用。當然的不只有 OpenCV,純 Rust 實作影像處理也有很多開源專案正在成長當中,雖然這已經是相當成熟的領域,但我相信未來仍有驚喜。

持續增加我們手中的工具吧!
https://github.com/liren0907/rust_one


上一篇
「Day 05」一目萬行,過目不忘:polars
下一篇
「Day 07」探索 AI 框架
系列文
30天解鎖 Rust 開發者工具箱9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言