:dep image
:dep num-traits
:dep base64
//above line is magic for adding crate
use image::{GenericImageView, ImageBuffer, Luma, Pixel, Primitive};
use num_traits::NumCast;
RustによるシンプルなGuidedFilterの実装
はじめに
高速なエッジ保持平滑化フィルタとしてGuidedFilter[1]が広く知られています.この記事では輝度信号に限定したシンプルな実装を紹介します.なお,実装にはRustとimageクレートを用いました.
やったこと
- GuidedFilterを用いたエッジ保持平滑化フィルタを実装
GuidedFilterについて
GuidedFilterはガイド信号\(\bf{I}\)の1次式で入力信号\(\bf{p}\)を近似することで出力信号\(\bf{q}\)を得ます.また,このガイド信号の選択により様々な応用が考案されています.今回紹介するエッジ保持平滑化フィルタはその中の一つです.
それでは,実際に出力信号を得るための方法について述べていきます.まず,\(\bf{q}\)と\(\bf{I}\)との関係を以下のように定義します.
\[ \begin{aligned} q_{i} & = a_{k} I_{i} + b_{k} \ ,\forall i \in \omega_{k} \\ \end{aligned} \]
ここで,\(i\)は画素のインデックスを,\(k\)はウィンドウ中心画素のインデックスを,\(\omega_{k}\)はウィンドウをそれぞれ表します.
次に,\(p\)と\(q\)との関係について考えます.ここでノイズ\(n\)を用いると以下のように表現することができます.
\[ \begin{aligned} q_{i} & = p_{i} - n_{i} \\ & = a_{k} I_{i} + b_{k} \end{aligned} \]
そして,\(q\)で\(p\)をより良く近似するため,以下の評価関数の最小化を考えます.
\[ \begin{aligned} E\left(a_{k}, b_{k}\right) & = \sum_{i \in \omega_{k}} \left(\left(a_{k}I_{i} + b_{k} - p_{i}\right)^{2} + \epsilon a_{k}^{2}\right) \\ \end{aligned} \]
この式において,\(\epsilon a_{k}^{2}\)で表される項は,\(a_{k}\)の正則化を目的としたものです.この項はリッジ回帰に用いられる正則化項と同様のモチベーションで導入されています.
それでは,\(E\left(a_{k}, b_{k}\right)\)を\(a_{k}\)および\(b_{k}\)で偏微分して最適な\(a_{k}\)と\(b_{k}\)を求めていきます.最初に\(b_{k}\)について考えます.
\[ \begin{aligned} \frac{\partial}{\partial b_{k}} E\left(a_{k}, b_{k}\right) & = 2\sum_{i \in \omega_{k}} \left(a_{k}I_{i} + b_{k} - p_{i}\right) \\ &= 0 \\ b_{k} &= \frac{1}{\left| \omega_{k} \right|} \sum_{i \in \omega_{k}} p_{i} - \frac{a_{k}}{\left| \omega_{k} \right|}\sum_{i \in \omega_{k}} I_{i}\\ \end{aligned} \]
ここで,\(\bar{p_{k}} = \frac{1}{\left| \omega_{k} \right|} \sum_{i \in \omega_{k}} p_{i}\), \(\mu_{k} = \frac{1}{\left| \omega_{k} \right|}\sum_{i \in \omega_{k}} I_{i}\) とすると,
\[ \begin{aligned} b_{k} &= \bar{p_{k}} - a_{k} \mu_{k} \end{aligned} \]
次に\(a_{k}\)について考えます.
\[ \begin{aligned} \frac{\partial}{\partial a_{k}} E\left(a_{k}, b_{k}\right) & = \frac{\partial}{\partial a_{k}} \sum_{i \in \omega_{k}} \left(\left(a_{k}I_{i} + b_{k} - p_{i}\right)^{2} + \epsilon a_{k}^{2}\right)\\ &= \frac{\partial}{\partial a_{k}} \sum_{i \in \omega_{k}} \left(\left(a_{k}I_{i} + \bar{p_{k}} - a_{k} \mu_{k} - p_{i}\right)^{2} + \epsilon a_{k}^{2}\right)\\ &= \frac{\partial}{\partial a_{k}} \sum_{i \in \omega_{k}} \left(\left(a_{k} \left( I_{i} -\mu_{k}\right) + \bar{p_{k}} - p_{i}\right)^{2} + \epsilon a_{k}^{2}\right)\\ &= 2\sum_{i \in \omega_{k}} \left(\left(a_{k} \left( I_{i} -\mu_{k}\right) + \bar{p_{k}} - p_{i}\right)\left( I_{i} -\mu_{k}\right) + \epsilon a_{k}\right)\\ &= 2a_{k} \sum_{i \in \omega_{k}} \left( I_{i} -\mu_{k}\right)^{2} - 2 \sum_{i \in \omega_{k}} p_{i}I_{i} + 2 \mu_{k} \sum_{i \in \omega_{k}} p_{i} + 2 \bar{p_{k}} \sum_{i \in \omega_{k}} I_{i} -2 \bar{p_{k}} \mu_{k} \left|\omega_{k}\right| + 2 \epsilon a_{k} \left| \omega_{k} \right| \\ &= 2a_{k} \sigma_{k}^{2} \left|\omega_{k}\right| - 2 \sum_{i \in \omega_{k}} p_{i}I_{i} + 2 \mu_{k}\bar{p_{k}}\left|\omega_{k}\right| + 2 \bar{p_{k}} \mu_{k}\left|\omega_{k}\right| -2 \bar{p_{k}} \mu_{k} \left|\omega_{k}\right| + 2 \epsilon a_{k} \left| \omega_{k} \right| \\ &= 0 \\ a_{k} &= \frac{ \frac{1}{\left|\omega_{k}\right|} \sum_{i \in \omega_{k}} p_{i}I_{i} - \bar{p_{k}} \mu_{k} }{\sigma_{k}^{2} + \epsilon } \end{aligned} \]
ここで,再度\(q_{i}\)について考えます.\(q_{i}\)は\(a_{k}\)及び\(b_{k}\)に依存していますが,これらの値はウィンドウ\(\omega_{k}\)の中心座標\(k\)に対して変化します.従って,\(q_{i}\)を一意に定めることはできません.そこで,\(q_{i}\)に影響を及ぼす\(\omega_{k}\)対して平均値を計算します.
\[ \begin{aligned} q_{i} & = \frac{1}{\left|\omega_{i}\right|} \sum_{k \in \omega_{i}} \left(a_{k} I_{i} + b_{k}\right) \\ &= \left(\frac{1}{\left| \omega_{i} \right|} \sum_{k \in \omega_{i}}a_{k} \right)I_{i} + \frac{1}{\left| \omega_{i} \right|} \sum_{k \in \omega_{i}}b_{k} \\ &= \bar{a_{i}} I_{I} + \bar{b_{i}} \end{aligned} \]
Rustによる実装
それでは,実際にRustでGuidedFilterを実装していきます.今回,以下のクレートを使用します.
image
は画像の読み書き及びデータ表現に,num-traits
は画像データ型から計算用のデータ型への変換に,base64
はNotebook上への画像出力にそれぞれ用いました.
まず初めに必要なクレートを導入します.
次にNotebook上への画像出力機能を実装します.詳細は以前の記事を参考にしてください.
extern crate image;
extern crate base64;
pub trait EvcxrResult {fn evcxr_display(&self);}
impl EvcxrResult for ImageBuffer<Luma<u8>, Vec<u8>> {
fn evcxr_display(&self) {
let mut buffer = Vec::new();
image::codecs::jpeg::JpegEncoder::new(&mut buffer).encode(&**self, self.width(), self.height(),
image::ColorType::L8).unwrap();
let img = base64::encode(&buffer);
println!("EVCXR_BEGIN_CONTENT image/jpeg\n{}\nEVCXR_END_CONTENT", img);
}
}
次にあると便利な雑多な関数を実装します.ここでは,所望のデータ型をSubpixel
としたPixel
型を取得するcast_subpixel
と スケールを調整した\(\epsilon\) を計算するcalc_eps
を実装します.論文中では画像データを\(0 \sim 1\)に正規化した状態で取り扱っています.今回の実装では,画像データを\(0 \sim 255\) の範囲で取り扱うため,calc_eps
で\(\epsilon\)の値を適切にスケーリングします.
fn cast_subpixel<S, T>(pixel: &Luma<S>) -> Luma<T>
where
: Primitive,
S: Primitive,
T{
let Luma([data]) = *pixel;
NumCast::from(data).unwrap(); 1])
Luma([}
fn cals_eps(eps: f64) -> f64 {
let eps = 255.0 * eps;
* eps
eps }
次に2次元平滑化処理を行うmean2d
を実装します.2次元平滑化処理は
- x方向移動平均フィルタ
- 転置
- x方向移動平均フィルタ
- 転置
という処理で実現しています.
fn mean2d<I, S>(image: &I, r: u32) -> ImageBuffer<Luma<f64>, Vec<f64>>
where
: GenericImageView<Pixel = Luma<S>>,
I: Primitive + 'static,
S{
fn get_luma_as_f64<I, S>(image: &I, x: u32, y: u32) -> f64
where
: GenericImageView<Pixel = Luma<S>>,
I: Primitive + 'static,
S{
let Luma(data) = image.get_pixel(x, y);
NumCast::from(data[0]).unwrap()
}
fn calc_mean(head_value: f64, tail_value: f64, r: u32) -> f64 {
let den = 2.0 * r as f64 + 1.0;
let num = head_value - tail_value;
/ den
num }
fn meand2d_tr<I, S>(image: &I, r: u32) -> ImageBuffer<Luma<f64>, Vec<f64>>
where
: GenericImageView<Pixel = Luma<S>>,
I: Primitive + 'static,
S{
let (width, height) = image.dimensions();
let mut result = ImageBuffer::new(height, width);
for y in 0..height {
let mut head_x: u32 = 0;
let mut mid_x: u32 = 0;
let mut tail_x: u32 = 0;
let mut acc_head_x: f64 = 0.0;
let mut acc_tail_x: f64 = 0.0;
while mid_x < width {
if head_x < width {
+= get_luma_as_f64(image, head_x, y);
acc_head_x += 1;
head_x }
if r < mid_x {
+= get_luma_as_f64(image, tail_x, y);
acc_tail_x += 1;
tail_x }
if r <= head_x {
let pixel = Luma([calc_mean(acc_head_x, acc_tail_x, r); 1]);
.put_pixel(y, mid_x, pixel);
result+= 1;
mid_x }
}
}
result}
&meand2d_tr(image, r), r)
meand2d_tr(}
実際にGuidedFilterの処理を行う関数guided_filter
です.今回はエッジ保持平滑化フィルタのみを対象としているため,\(I = p\)(ガイド信号は入力信号と等しい)としています.また,r
はウィンドウ領域の半径を,eps
は\(\epsilon\)をそれぞれ表します.
fn guided_filter<I, S>(image: &I, r: u32, eps: f64) -> ImageBuffer<Luma<u8>, Vec<u8>>
where
: GenericImageView<Pixel = Luma<S>>,
I: Primitive + 'static,
S{
let eps = cals_eps(eps);
let i = image;
let mean_i = mean2d(i, r);
let (w, h) = i.dimensions();
let ii = ImageBuffer::from_fn(w, h, |x, y| {
&i.get_pixel(x, y)).map(|v: f64| v * v)
cast_subpixel(});
let corr_i = mean2d(&ii, r);
let var_i = ImageBuffer::from_fn(w, h, |x, y| {
mean_i.get_pixel(x, y)
.map2(corr_i.get_pixel(x, y), |m, c| c - m * m)
});
let a = ImageBuffer::from_fn(w, h, |x, y| {
.get_pixel(x, y).map_without_alpha(|v| v / (v + eps))
var_i});
let b = ImageBuffer::from_fn(w, h, |x, y| {
mean_i.get_pixel(x, y)
.map2(a.get_pixel(x, y), |m, a| (1.0 - a) * m)
});
let mean_a = mean2d(&a, r);
let mean_b = mean2d(&b, r);
let q = ImageBuffer::from_fn(w, h, |x, y| {
let i_v: f64 = {
let Luma([data]) = i.get_pixel(x, y);
NumCast::from(data).unwrap()
};
mean_a.get_pixel(x, y)
.map2(mean_b.get_pixel(x, y), |a, b| a * i_v + b)
});
ImageBuffer::from_fn(w, h, |x, y| -> Luma<u8> {
.get_pixel(x, y))
cast_subpixel(q})
}
実行結果
入力信号は以下のとおりです.
let img = image::open("../assets/img/2021-09-30-simple-guided-filter-rs/media/input.jpg").unwrap().to_luma8();
img
r=18, eps=0.1^2
&img, 18, 0.1) guided_filter(
r=18, eps=0.4^2
&img, 18, 0.4) guided_filter(
r=18, eps=0.8^2
&img, 18, 0.8) guided_filter(