2018年2月10日土曜日

Rustで画像の輪郭(canny edge)を取得してみた


背景

画像処理の実装について個人的に興味を持っていたので、canny edgeを利用した画像の輪郭抽出をRustで実装してみました。

全体像

  1. 使ったもの
  2. canny edgeとは
  3. 画像を白黒化
  4. ガウシアンフィルタを適用
  5. 強さと角度を取得
  6. エッジを取得
  7. エッジをPixbufに変換
  8. 動作確認
  9. まとめ

使ったもの


canny edgeとは

canny edge(キャニーエッジ)とは、輪郭抽出として利用できる画像処理の手法です。
画像処理ライブラリとして有名なOpenCVでも実装されています。

canny edgeは、このように処理することで取得できます。

  1. 白黒画像を用意
  2. ノイズ除去のために、ガウシアンフィルタを適用して、画像をぼかす
  3. ぼかした画像の各ピクセルに対して、変化の強さと方向を求める
  4. 変化の頂点(エッジ)を取得

それぞれの実装内容を説明します。

画像を白黒化

ベースとして利用したプロジェクトでは、画像をPixbufという型で扱っています。
Pixbufのままでは扱いにくいので、白黒化と共にndarrayの2次元配列にしました。
白黒化は「gray = 0.299*R + 0.587*G + 0.114*B」という式で変換しました。
(参考: OpenCVWikipedia

fn pixbuf_to_gray_mat(color_pixbuf: &Pixbuf) -> Array2<f32> {
    let mut gray_pixels = vec![];
    let pixels;
    unsafe {
        pixels = color_pixbuf.get_pixels();
    }
    for rgb in pixels.chunks(3) {
        let pixel =
            0.299 * rgb[0] as f32 +
            0.587 * rgb[1] as f32 +
            0.114 * rgb[2] as f32;
        gray_pixels.push(pixel);
    }
    let w = color_pixbuf.get_width() as usize;
    let h = color_pixbuf.get_height() as usize;
    Array::from_vec(gray_pixels).into_shape((h, w)).unwrap()
}

ガウシアンフィルタを適用

5x5のガウシアンフィルタを適用して、画像をぼかしました。
ndarrayのArray2の幅と高さを他の関数でも使うため、幅と高さ取得用の関数も定義しました。
fn array2_size(array2: &Array2<f32>) -> (usize, usize) {
    let shape = array2.shape();
    (shape[1], shape[0])
}

fn apply_gaussian_filter(gray_mat: &Array2<f32>) -> Array2<f32> {
    let mut blur_vec = vec![];
    let edge_detect_mat = arr2(
        &[[2.,  4.,  5.,  4., 2.],
          [4.,  9., 12.,  9., 4.],
          [5., 12., 15., 12., 5.],
          [4.,  9., 12.,  9., 4.],
          [2.,  4.,  5.,  4., 2.]]);
    let base = edge_detect_mat.scalar_sum();
    let (w, h) = array2_size(&gray_mat);
    for i in 0..h {
        for j in 0..w {
            if i<3 || h-4<i || j<3 || w-4<j {
                blur_vec.push(*gray_mat.get((i, j)).unwrap());
            } else {
                let mat = gray_mat.slice(s![i-2..i+3, j-2..j+3]);
                let mat = mat.mul(&edge_detect_mat);
                let pixel = mat.scalar_sum() / base;
                blur_vec.push(pixel);
            }
        }
    }
    Array::from_vec(blur_vec).into_shape((h, w)).unwrap()
}


強さと角度を取得

各ピクセルに対して、変化の強さと角度を求めました。
ピクセルで比較できる角度は45度なので、取得する角度もそれに合わせています。
fn get_rough_angle(angle: f32) -> i32 {
    if ((angle > 22.5) && (angle < 67.5)) || ((angle < -112.5) && (angle > -157.5)) {
        45
    } else if ((angle > 67.5) && (angle < 112.5)) || ((angle < -67.5) && (angle > -112.5)) {
        90
    } else if ((angle > 112.5) && (angle < 157.5)) || ((angle < -22.5) && (angle > -67.5)) {
        135
    } else {
        0
    }
}

fn get_strength_and_angle(gray_mat: &Array2<f32>) -> (Vec<f32>, Vec<i32>) {
    let (w, h) = array2_size(&gray_mat);
    let gx_mat = arr2(&[[-1.,  0.,  1.],
                        [-2.,  0.,  2.],
                        [-1.,  0.,  1.]]);
    let gy_mat = arr2(&[[-1., -2., -1.],
                        [ 0.,  0.,  0.],
                        [ 1.,  2.,  1.]]);
    let mut strength_vec = vec![];
    let mut angle_vec = vec![];
    for i in 0..h {
        for j in 0..w {
            if i<2 || h-3<i || j<2 || w-3<j {
                strength_vec.push(0.);
                angle_vec.push(0);
            } else {
                let mat = gray_mat.slice(s![i-1..i+2, j-1..j+2]);
                let gx = mat.mul(&gx_mat).scalar_sum();
                let gy = mat.mul(&gy_mat).scalar_sum();
                let strength = (gx.powf(2.) + gy.powf(2.)).sqrt();
                strength_vec.push(strength);
                let raw_angle = ((gx/gy).atan() / std::f32::consts::PI) * 180.;
                angle_vec.push(get_rough_angle(raw_angle));
            }
        }
    }
    (strength_vec, angle_vec)
}


エッジを取得

変化の頂点をエッジと判定し、エッジを白(255)、そうでなければ黒(0)で値を返しました。
小さな勾配の頂点をエッジとすると期待しない値となる可能性が高いため、勾配が10未満のピクセルは、エッジから除外しました。
fn is_edge_pixel(current_x: usize, current_y: usize, image_w: usize, strength_vec: <f32>, compare_angle: i32) -> bool {
    let current_index = image_w * current_y + current_x;
    let (compare_index1, compare_index2) =
        match compare_angle {
            45 =>
                (image_w * current_y + current_x + 1,
                 image_w * current_y + current_x - 1),
            90 =>
                (image_w * (current_y - 1) + current_x - 1,
                 image_w * (current_y + 1) + current_x + 1),
            135 =>
                (image_w * (current_y - 1) + current_x - 1,
                 image_w * (current_y + 1) + current_x + 1),
            _ =>
                (image_w * (current_y - 1) + current_x,
                 image_w * (current_y + 1) + current_x),
        };
    let current_strength = strength_vec[current_index];
    let compare_strength1 = strength_vec[compare_index1];
    let compare_strength2 = strength_vec[compare_index2];
    current_strength > compare_strength1 && current_strength > compare_strength2
}

fn get_edge(strength_vec: &Vec<f32>, angle_vec: &Vec<i32>, w: usize, h: usize) -> Array2<f32> {
    let mut edge_vec = vec![];
    for i in 0..h {
        for j in 0..w {
            if i<2 || h-3<i || j<2 || w-3<j {
                edge_vec.push(0.);
            } else {
                let this_index = w * i + j;
                let result =
                    if strength_vec[this_index] > 10. && is_edge_pixel(j, i, w, &strength_vec, angle_vec[this_index]) {
                        255.
                    } else {
                        0.
                    };
                edge_vec.push(result);
            }
        }
    }
    Array::from_vec(edge_vec).into_shape((h, w)).unwrap()
}


エッジをPixbufに変換

gtkのImageで表示するために、エッジが記されたArray2をPixbufに変換しました。
fn mat_to_pixbuf(edge_mat: Array2<f32>) -> Pixbuf {
    let (uw, uh) = array2_size(&edge_mat);
    let iw = uw as i32;
    let ih = uh as i32;
    let mut gray_rgb_vec = vec![];
    for p in edge_mat.into_shape((uw * uh)).unwrap().to_vec() {
        let p = p as u8;
        gray_rgb_vec.extend_from_slice(&[p, p, p])
    }
    Pixbuf::new_from_vec(
        gray_rgb_vec,
        0, // pixbuf supports only RGB
        false,
        8,
        iw, ih, iw * 3)
}

動作確認

説明した処理を利用して、webカメラで取得した画像に対してcanny edgeを適用するプログラムを作りました。

こちらのリポジトリで公開しています。
asukiaaa/rust_canny_edge_camera

リポジトリのディレクトリで下記のコマンドを実行すること、プログラムが動きます。
cargo run


「toggle camera」ボタンをクリックすると、画像取得とエッジ検出が行われます。


輪郭(エッジ)を検出できました。
しかし、並列処理をしてないので、640x360pxの画像処理に6秒くらいかかります。
何かに利用する場合は、並列化などの速度向上が必要そうです。

まとめ

並列化してないので時間はかかりますが、Rustで画像の輪郭を取得できました。

何かの参考になれば嬉しいです。

参考

asukiaaa/rust_canny_edge_camera
Canny Edge Detector
Canny Tutorial

0 件のコメント :