2018年3月7日水曜日

Rustでステレオ画像のブロックマッチングをしてみた


背景

ステレオ画像(Wiki: ステレオグラム)とは、立体的な印象を持つように撮影された画像です。
人の左右の目のようにカメラの位置をずらして撮影すると、そのような画像を得られます。

ブロックマッチングとは、2枚の画像の差をピクセルではなくブロック単位で取得する手法であり、ステレオ画像から立体情報を取得する方法の一つです。
画像処理ライブラリであるOpenCVにも、stereoBMという関数が実装されています。
日本語の記事: ステレオ画像から距離計測
apiドキュメント: cv::StereoBM Class Referencecv::cuda::StereoBM Class Reference

ブロックマッチングを利用したステレオ画像処理の実装に興味があったので、Rustでやってみました。
備忘録として、やったことを共有します。

全体像

  1. 使ったもの
  2. ブロックマッチングの概要
  3. 並列処理の無いブロックマッチング
  4. OpenCLでGPUを利用して、並列に処理するブロックマッチング
  5. 早さの比較
  6. まとめ

使ったもの


ブロックマッチングの概要

下記の情報を利用して、最も似ているブロックとの視差(今回の場合はピクセル数)を求めるのがブロックマッチングです。
  • 左右の白黒画像
  • 比較単位であるブロックの幅と高さ
  • 比較する距離の最大値
求めたブロックの視差が、小さかったら近くにある、大きかったら遠くにある、と推測できます。

こちらの450x375pxの左右の画像に対して11x11pxのブロックを118px(450px/4)の幅で探索を行います。



探索の結果、このような画像を作成するプログラムを作ります。
色の意味は赤は近距離、緑は中くらいの距離、青は遠距離を意味します。


並列処理のないブロックマッチング

並列処理は考えず、rustのvecでブロックマッチングを実装してみました。

ImageというCrateを利用して、左右の白黒画像を作成しました。
fn get_gray_pixels(file_name: &str) -> (Vec, usize, usize) {
    let img = image::open(file_name).unwrap().grayscale();
    (img.raw_pixels(), img.width() as usize, img.height() as usize)
}

fn main() {
    let left_image_file_name = "data/left.png";
    let right_image_file_name = "data/right.png";
    let (left_pixels, width, height) = get_gray_pixels(&left_image_file_name);
    let (right_pixels, _, _) = get_gray_pixels(&right_image_file_name);
    ..
}

ブロックマッチングに使うブロックのサイズは11x11としました。 最大視差は幅の1/4にしました。
let block_w = 11;
let block_h = 11;
let diff_len = w/4;

それぞれのブロックに対して最も差が小さい視差を求めました。
fn get_min_diff_index(left_pixels: &Vec, right_pixels: &Vec, w: usize, block_w: usize, block_h: usize, block_x: usize, block_y: usize, diff_len: usize) -> f32 {
    let mut min_diff_point = std::f32::MAX;
    let mut min_diff_index = diff_len;
    for diff_index in 0..diff_len {
        let mut diff_point:f32 = 0.0;
        for x in (block_w * block_x)..(block_w * (block_x + 1)) {
            for y in (block_h * block_y)..(block_h * (block_y + 1)) {
                diff_point += (left_pixels[y * w + x + diff_index] as f32 - right_pixels[y * w + x] as f32).abs();
            }
        }
        if diff_point < min_diff_point {
            min_diff_point = diff_point;
            min_diff_index = diff_index;
        }
    }
    min_diff_index as f32
}

fn block_match(left_pixels: &Vec, right_pixels: &Vec, w: usize, h: usize, block_w: usize, block_h: usize, diff_len: usize) -> Vec {
    let result_w = w / block_w;
    let result_h = h / block_h;
    let mut diff_vec = vec![];
    for y in 0..result_h {
        for x in 0..result_w {
            diff_vec.push(get_min_diff_index(&left_pixels, &right_pixels, w, block_w, block_h, x, y, diff_len));
        }
    }
    diff_vec
}

fn main() {
    ..
    let result_pixels = block_match(&left_pixels, &right_pixels, width, height, block_w, block_h, diff_len);
    ..
}

求めた結果の配列は、視差が近いと赤くなり、遠くなるほど赤->緑->青に変化するように色づけして、Imageを通してpng画像として保存ました。
fn hsv_to_rgb(h: u8, s: u8, v: u8) -> Vec {
    let hf = (h as f32 * 360. / std::u8::MAX as f32) / 60.;
    let sf = s as f32 / std::u8::MAX as f32;
    let vf = v as f32;
    let h_floor = hf.floor();
    let ff = hf - h_floor;
    let p = (vf * (1. - sf)) as u8;
    let q = (vf * (1. - sf * ff)) as u8;
    let t = (vf * (1. - sf * (1. - ff))) as u8;

    match h_floor as u8 {
        0 => vec![v, t, p],
        1 => vec![q, v, p],
        2 => vec![p, v, t],
        3 => vec![p, q, v],
        4 => vec![t, p, v],
        5 => vec![v, p, q],
        6 => vec![v, t, p],
        _ => vec![0, 0, 0],
    }
}

fn main() {
    ..
    let mut pixels = vec![];
    let diff_len_f32 = diff_len as f32;
    for p in result_pixels {
        let h = ((diff_len_f32 - p) / diff_len_f32) * 200.0;
        pixels.extend(hsv_to_rgb(h as u8, 255, 255));
    }
    let result_image = RgbImage::from_raw((width / block_w) as u32, (height / block_h) as u32, pixels).unwrap();
    let _saved = result_image.save("result.png");
}

作ったプログラムはgithubで公開しています。
rust_stereo_block_matching_practice/with_vec/src/main.rs

下記のようなコマンドで実行できます。
git clone https://github.com/asukiaaa/rust_stereo_block_matching_practice.git
cd rust_stereo_block_matching_practice/with_vac
cargo run

OpenCLでGPUを利用して、並列に処理するブロックマッチング

oclというcrateを利用すると、RustからOpenCLを通してGPUを利用できるので、OpenCLを利用した並列処理ブロックマッチングも作ってみました。

並列処理しないときと同じように、ImageというCrateで左右の画像を読み込みます。
fn get_gray_pixels(file_name: &str) -> (Vec<u8>, usize, usize) {
    let img = image::open(file_name).unwrap().grayscale();
    (img.raw_pixels(), img.width() as usize, img.height() as usize)
}

fn main() {
    let left_image_file_name = "data/aloeL.jpg";
    let right_image_file_name = "data/aloeR.jpg";
    // let left_image_file_name = "data/left.png";
    // let right_image_file_name = "data/right.png";
    let (left_pixels, width, height) = get_gray_pixels(&left_image_file_name);
    let (right_pixels, _, _) = get_gray_pixels(&right_image_file_name);
    let block_w = 11;
    let block_h = 11;
    let diff_len = width / 4;
    ..
}

差分の計算とブロックの取得をOpenCLのカーネルの関数として記述して、その文字列をoclに渡します。
fn main() {
    ..
    let src = r#"
        __kernel void get_diffs(
                     __global unsigned char* left_pixels,
                     __global unsigned char* right_pixels,
                     __global unsigned char* diffs,
                     size_t w,
                     size_t h,
                     size_t diff_len) {
            size_t x = get_global_id(0);
            size_t y = get_global_id(1);
            size_t target_index = y * w + x;
            size_t diff_index;
            for (diff_index = 0; diff_index < diff_len; ++diff_index) {
                unsigned char left = left_pixels[target_index + diff_index];
                unsigned char right = right_pixels[target_index];
                unsigned char value;
                if (left > right)
                    value = left - right;
                else
                    value = right - left;
                diffs[target_index * diff_len + diff_index] = value;
            }
        }

        __kernel void get_result_diffs(
                     __global unsigned char* diffs,
                     __global unsigned char* result_diffs,
                     size_t w,
                     size_t h,
                     size_t block_w,
                     size_t block_h,
                     size_t result_w,
                     size_t result_h,
                     size_t diff_len) {
            size_t result_x = get_global_id(0);
            size_t result_y = get_global_id(1);
            if (result_x > result_w || result_y > result_h)
                return;
            size_t x, y, i;
            size_t min_diff_index;
            unsigned int min_diff_point;
            for (i = 0; i < diff_len; i++) {
                unsigned int diff_point = 0;
                for (x = result_x * block_w; x < (result_x + 1) * block_w; x++) {
                    for (y = result_y * block_h; y < (result_y + 1) * block_h; y++) {
                        diff_point += (unsigned int) diffs[(y * w + x) * diff_len + i];
                    }
                }
                if (i == 0 || min_diff_point > diff_point) {
                    min_diff_index = i;
                    min_diff_point = diff_point;
                }
            }
            result_diffs[result_y * result_w + result_x] = min_diff_index;
        }
    "#;

    let global_work_size = SpatialDims::new(Some(width),Some(height),Some(1)).unwrap();
    let pro_que = ProQue::builder()
        .src(src)
        .dims(global_work_size)
        .build().expect("Build ProQue");
    ..
}

画像や中間生成物や出力用のOpenCLのバッファを確保します。
main() {
    ..
    let left_pixels_buffer = Buffer::builder()
        .queue(pro_que.queue().clone())
        .flags(MemFlags::new().read_write().copy_host_ptr())
        .len(width * height)
        .host_data(&left_pixels)
        .build().unwrap();

    let right_pixels_buffer = Buffer::builder()
        .queue(pro_que.queue().clone())
        .flags(MemFlags::new().read_write().copy_host_ptr())
        .len(width * height)
        .host_data(&right_pixels)
        .build().unwrap();

    let diffs_buffer: Buffer<u8> = Buffer::builder()
        .queue(pro_que.queue().clone())
        .flags(MemFlags::new().read_write())
        .len(width * height * diff_len)
        .build().unwrap();

    let result_w = width / block_w;
    let result_h = height/ block_h;

    let result_diffs_buffer: Buffer<u8> = Buffer::builder()
        .queue(pro_que.queue().clone())
        .flags(MemFlags::new().read_write())
        .len(result_w * result_h)
        .build().unwrap();
    ..
}

確保したバッファをカーネルに紐付けて実行します
main() {
    ..
    let get_diffs_kernel = pro_que.create_kernel("get_diffs").unwrap()
        .arg_buf(&left_pixels_buffer)
        .arg_buf(&right_pixels_buffer)
        .arg_buf(&diffs_buffer)
        .arg_scl(width)
        .arg_scl(height)
        .arg_scl(diff_len);

    unsafe { get_diffs_kernel.enq().unwrap(); }

    let get_result_diffs_kernel = pro_que.create_kernel("get_result_diffs").unwrap()
        .arg_buf(&diffs_buffer)
        .arg_buf(&result_diffs_buffer)
        .arg_scl(width)
        .arg_scl(height)
        .arg_scl(block_w)
        .arg_scl(block_h)
        .arg_scl(result_w)
        .arg_scl(result_h)
        .arg_scl(diff_len);

    unsafe { get_result_diffs_kernel.enq().unwrap(); }
    ..
}

カーネルを実行したら、バッファからデータを取り出します。
main() {
    ..

    let mut result_diffs = vec![0; result_diffs_buffer.len()];
    result_diffs_buffer.read(&mut result_diffs).enq().unwrap();
    ..
}

結果を取得できたら、vecでの処理と同じように色付けします。
main() {
    ..
    let mut pixels = vec![];
    let diff_len_f32 = diff_len as f32;
    for p in result_diffs {
        let h = ((diff_len_f32 - p as f32) / diff_len_f32) * 200.0;
        pixels.extend(hsv_to_rgb(h as u8, 255, 255));
    }
    let result_image = RgbImage::from_raw(result_w as u32, result_h as u32, pixels).unwrap();
    let _saved = result_image.save("result.png");
}

作ったプログラムはgithubで公開しています。
rust_opencl_block_matching_practice/2dims_work_size_with_loop_in_kernel/src/main.rs

下記のようなコマンドで実行できます。
git clone https://github.com/asukiaaa/rust_opencl_block_matching_practice
cd rust_opencl_block_matching_practice/2dims_work_size_with_loop_in_kernel
cargo run

早さの比較

無並列版とOpenCLを利用した並列処理版の早さを比較してみました。
動作確認は、以前の記事で環境を構築した、下記の環境で行いました。

分類名前
モデルLenovo ThinkPad S1 Yoga
CPUIntel Core i7-4600U CPU @ 2.10GHz × 4
GPUIntel Haswell Mobile
OSUbuntu17.10

450 x 375pxの画像の処理

利用したのは、こちらの画像です。
左: rust_stereo_block_matching_practice/data/left.png
右: rust_stereo_block_matching_practice/data/right.png

無並列処理
Load image PT0.604994989S sec
Get result PT2.609991636S sec
Create resutl image PT0.006862230S sec
Total PT3.221848855S sec

OpenCLを利用した並列処理
Load image PT0.622571324S sec
Put kernel PT0.082274205S sec
Create buffer PT0.000206853S sec
Get result PT0.013937184S sec
Create resutl image PT0.007915641S sec
Total PT0.726905207S sec

画像の読み書きの時間を差し引くと、無並列が12.6秒なのに対して、並列は0.1秒(0.082 + 0.0023 + 0.014)で計算できているので、120倍位早くなっています。

1282 x 1110pxの画像の処理

利用したのはこちらの画像です。
左: rust_stereo_block_matching_practice/data/aloeL.jpg
右: rust_stereo_block_matching_practice/data/aloeR.jpg

無並列処理
Load image PT3.250021343S sec
Get result PT61.742265897S sec
Create resutl image PT0.031577473S sec
Total PT65.023864713S sec

OpenCLを利用した並列処理
Load image PT3.345406494S sec
Put kernel PT0.098250854S sec
Create buffer PT0.001059544S sec
Get result PT0.382950297S sec
Create resutl image PT0.034919479S sec
Total PT3.862586668S sec

画像の読み書きの時間を差し引くと、無並列が62秒なのに対して、並列は0.48秒(0.098 + 0.0011 + 0.38)で計算できているので、120倍位早くなっています。

まとめ

前から内容に興味があったブロックマッチングを自分で実装できました。
また、OpenCLを利用すると、環境によっては処理速度が120倍位早くなることが分かりました。

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

参考

Stereo Vision Tutorial
PistonDevelopers/image
Algorithm to convert RGB to HSV and HSV to RGB in range 0-255 for both
Crate ocl

変更履歴

2018.03.08
T60を160と読み間違えてしまい、誤って300倍早くなったと説明していたので、120倍に修正しました。
失礼しました。

0 件のコメント :