Rustで画像のピクセルを編集して画像処理

はじめに

前回はPythonを使ってオリジナルな自作の画像加工を行いました。 Pythonはいろいろ便利な画像処理パッケージがそろって便利ですが、 生のピクセルを直接扱おうとしたときなどでは速度面で不安があります。 RustにもopenCV bindingクレートがありますが、https://crates.io/crates/opencv を見るとまだ導入が大変そうなので、 今回はimageクレートを使用します。前回と同じくハニカム模様のモザイク加工を行い、また新たにPerlin Noise(パーリンノイズ)を作成します。

以下はPerlin Noise(パーリンノイズ)を用いた画像例です。

今回作成したソースコードはhttps://gist.github.com/tk87s/ec42fd517697ed3507ac3ae7ec4a6715にあります。

Image crate

ImageクレートはRustの画像処理クレートの中で一番メジャーなものかと思いますが、 画像フォーマットのサポートもWebP形式などは対応していませんが、実用性としては十分にカバーしていると思います(Enum image::ImageFormatで確認可能)。 imageoptでガウシアンblurや色調の変換、リサイズ、フリップなど基本的な画像処理も可能ですが、多機能というほどではありません。 画像フォーマット間で変換を行いたいときやバイナリサイズを抑えたい場合、レイトレーシングなど速度を求めるときに使用することになるでしょう。

画像の読み込みと書き込みはファイル名から自動的にフォーマットを認識し、デコード・エンコードを行ってくれます。

    let img = image::open("./img_data/bird.jpg").unwrap();
img.save("bird.png").unwrap();
    

画像を扱う際にはImageBufferを使用することになります。 ImageBufferオブジェクトは画像のdimentionなどの基本的な情報の取得や、 ピクセルオブジェクトのget/set、iteratorにすることもできます。 以下ではピクセルオブジェクトとしてimage::Rgbを使用して着色しています。 ピクセルオブジェクトは他にはRgbaやLuma(グレースケール)、LumaAがあります(Enum image::ColorType)。

    let mut img = image::ImageBuffer::new(w, h);
let pixel = *img.get_pixel(w/2, h/2);
img.put_pixel(w/2, h/2, pixel);
for (x, y, pixel) in img.enumerate_pixels() {
    *pixel = image::Rgb([0, 127, 255]);
}
img.save("result.png").unwrap();
    

ハニカム模様を作る

前回ではPythonを使用して画像のハニカムモザイク加工を行いましたが、今回はRustを使用します。 高速に直接ピクセルを扱うことが可能となるので、 numpyを適用しにくいような加工ではPythonよりRustの方が速度面ではるかに有利になります。

まずハニカムの中心点を設定します。元の画像がある場合は画像の大きさを合わせるためにdimentionを引数に渡します。 ただの模様だけを得たい場合はデフォルトで300*300の大きさにしています。

    fn make_honeycomb_points(dim: Option<(u32, u32)>, n: u32) -> Vec<(u32, u32)> {
    let (w, h) = if let Some(dim) = dim {dim} else {(300,300)};
    let mut points = Vec::new();
    let dh = n / 2;
    let dw = n;
    let mut row = 0;
    while row * dh < h {
        let mut col = 0;
        while col * dw * 2 + row % 2 * dw < w {
            points.push((col * dw * 2 + row % 2 * dw, row * dh));
            col += 1;
        }
        row += 1;
    }
    points
}
    

中心点を起点として幅優先探索を使用しそれぞれの領域にindexでラベルを付けてから、ラベルごとに好きな色を設定します。

    let mut que = VecDeque::new();
for (i, &p) in points.iter().enumerate() {
    que.push_back((i, p));
}

let n = points.len();
let mut id_map = vec![vec![n; w as usize]; h as usize];

while let Some((id, po)) = que.pop_front() {
    for d in [!0, 0, 1, 0, !0].windows(2) {
        let x = po.0 + d[0];
        let y = po.1 + d[1];
        if x < w && y < h && id_map[y as usize][x as usize] == n {
            id_map[y as usize][x as usize] = id;
            que.push_back((id, (x, y)));
        }
    }
}
    

ランダムに色を付けたい場合は以下のようになります。 RGBごとに0-255の一様分布からランダムに色を取得しました。 各ピクセルごとに値を変更したい場合はenumerate_pixels_mut()が便利です。

    let mut rng = XorShiftRng::from_entropy();
let colors: Vec<(u8, u8, u8)> = (0..n)
    .map(|_| ((rng.gen_range(0..256)) as u8,(rng.gen_range(0..256)) as u8, (rng.gen_range(0..256)) as u8))
    .collect();

let mut img = image::ImageBuffer::new(w, h);
for (x, y, pixel) in img.enumerate_pixels_mut() {
    let id = id_map[y as usize][x as usize];
    *pixel = image::Rgb([colors[id].0, colors[id].1, colors[id].2]);
}
img
    

ある元の画像を基に色を決定したい場合は以下になります。 元の画像は以前にも使用した鳥の画像を使用し、領域ごとに元の画像の色の平均で着色しました。 指定の色に限定したい場合は、色を量子化してからmedianかmodeで色を設定するとよいかと思います。

    let mut new_img = image::ImageBuffer::new(w, h);
let mut count = vec![0u32; n];
let mut color_sum = vec![(0,0,0); n];

//img: base imagebuffer
for (x, y, pixel) in img.enumerate_pixels() {
    let id = id_map[y as usize][x as usize];
    count[id] += 1;
    let image::Rgb(color) = *pixel;
    color_sum[id].0 += color[0] as u32;
    color_sum[id].1 += color[1] as u32;
    color_sum[id].2 += color[2] as u32;
}

for (x, y, pixel) in new_img.enumerate_pixels_mut() {
    let id = id_map[y as usize][x as usize];
    *pixel = image::Rgb(
        [
            (color_sum[id].0 / count[id]) as u8,
            (color_sum[id].1 / count[id]) as u8,
            (color_sum[id].2 / count[id]) as u8,
        ]
    );
}
new_img
    

Perlin Noiseの作成

perlin noise(パーリンノイズ)は自然に見えるノイズを生成するための手法です。 まずグリッドメッシュを生成し、メッシュの交点ごとにランダムにそのノイズに適用する次元数のベクトルをランダムに設定した後、 それぞれのメッシュセル内にあるターゲット点とそのメッシュの頂点からの変位ベクトルと頂点に設定したランダムベクトルの内積を取ることによって、 そのターゲット点の輝度を決めます。全体として輝度をスムーズさを得るためにsmoothstep関数を設定して補完を行います。

補完は指定の区間において両端がスムーズであればよいので、smoothstep関数は[0,1]の定義域では0と1の点で勾配が0であればよいです。 今回のケースではx=0,1の時y=0,1かつ一次導関数でx=0,1の時0であればよいです。補完をよりスムーズにするため次数が奇数の高次多項式が採用されやすいです。 以下のようなものが例です。

  • $3x^{2}-2x^{3}$
  • $6x^{5}-15x^{4}+10x^{3}$

以下はRustによる二次元のPerlin noiseを生成させるオブジェクトの作成コードになります。 octaves変数がありますが、これはメッシュグリッドの大きさを元のスケールに対して 2^octave ごとの割合で小さくして足し合わせることで、 様々なスケールでのノイズを生成することができ自然なノイズを取得することが可能になります。

最終的にそれぞれの点に対して-1.0-1.0の値が得られますので、 画像の輝度を1バイトに変換するには1を足して2で割り、255を掛けてu8型にすればよいです。

    impl PerlinNoise {
    pub fn new(octaves: usize) -> Self {
        Self {
            octaves: octaves,
            scale: 2.0 * 2.0_f64.powf(-0.5),
            gradient: HashMap::new(),
            rng: XorShiftRng::from_entropy(),
        }
    }

    pub fn smootherstep(t: f64) -> f64 {
        6.0 * t.powf(5.0) - 15.0 * t.powf(4.0) + 10.0 * t.powf(3.0)
    }

    pub fn lerp(t: f64, a: f64, b: f64) -> f64 {
        a + t * (b - a)
    }

    pub fn get_gradient(&mut self) -> (f64, f64) {
        let x: f64 = self.rng.sample(StandardNormal);
        let y: f64 = self.rng.sample(StandardNormal);
        let norm = (x*x + y*y).powf(-0.5);
        (x * norm, y * norm)
    }

    fn get_dot(&mut self, x: u32, y: u32) -> (f64, f64) {
        if !self.gradient.contains_key(&(x, y)) {
            let grad = self.get_gradient();
            self.gradient.insert((x, y), grad);
        }
        *self.gradient.get(&(x, y)).unwrap()
    }

    pub fn get_noise(&mut self, point: (f64, f64)) -> f64 {
        let x = (point.0.floor() as u32, point.0.floor() as u32 + 1);
        let y = (point.1.floor() as u32, point.1.floor() as u32 + 1);

        let (p1, p2) = self.get_dot(x.0, y.0);
        let d00 = p1 * (point.0 - x.0 as f64) + p2 * (point.1 - y.0 as f64);

        let (p1, p2) = self.get_dot(x.1, y.0);
        let d10 = p1 * (point.0 - x.1 as f64) + p2 * (point.1 - y.0 as f64);

        let (p1, p2) = self.get_dot(x.0, y.1);
        let d01 = p1 * (point.0 - x.0 as f64) + p2 * (point.1 - y.1 as f64);

        let (p1, p2) = self.get_dot(x.1, y.1);
        let d11 = p1 * (point.0 - x.1 as f64) + p2 * (point.1 - y.1 as f64);

        let (sx, sy) = (Self::smootherstep(point.0 - point.0.floor()), Self::smootherstep(point.1 - point.1.floor()));
        Self::lerp(sy, Self::lerp(sx, d00, d10), Self::lerp(sx, d01, d11)) * self.scale
    }
    
    pub fn get_perlin(&mut self, point: (f64, f64)) -> f64 {
        let mut ret = 0.0;
        for i in 0..self.octaves {
            let i2 = (1u32 << i) as f64;
            let np1 = point.0 * i2;
            let np2 = point.1 * i2;
            ret += self.get_noise((np1, np2)) / i2;
        }
        ret /= 2.0 - 2.0_f64.powf(1.0 - self.octaves as f64);
        ret
    }
}
    

生成した画像はそれぞれRGBに対して単一スケール(octaves=1)のperlin noiseオブジェクトを作成し、 処理を行った画像になります。それぞれの点における値は0.0-5.0に設定しているのでグリッドの数は5*5の25個、画像の幅と高さに対して、 周期は5になっていることがわかります。

まとめ

Pythonでそれぞれのpixelにアクセスするのは速度面で抵抗がありましたが、 Rustはその辺りには抵抗はないですしImage crateはシンプルで使いやすい印象です。 結構面白いので、次回はまた別のよく使用するけどimage crateにはないような処理を行います。

Reference

  1. https://crates.io/crates/image
  2. https://en.wikipedia.org/wiki/Perlin_noise
Next Post Previous Post
No Comment
Add Comment
comment url