🦀

Rustで画像処理 メリットと注意点

2023/12/08に公開

はじめに

こんにちは、kackyと申します。Rust を業務で使うエンジニアの皆様に向けて、Rust で画像処理をするメリットや実装する上での注意点を共有します。

Rustで画像処理を行うメリット

マルチプラットフォーム対応

Rustは多様なプラットフォームに対応しています。特にWebAssemblyへの対応がスムーズです。C++など他の言語と比べても、Rustのこの特性は大きな利点です。ただし、クレートの利用時には、完璧な品質が保証されるわけではないため、一定の妥協が必要になる場合もあります。

並列化の対応が容易

画像処理のパフォーマンス向上のためにrayonのイテレータを使うことで容易にマルチコアを利用した並列化環境を構築できます。また、プラットフォームによってマルチコアを利用できない場合、例えばWebAssemblyなどの環境ではフィーチャーフラグを利用して並列化のon/offを切り替えることができるところがRustを利用する強みになります。

オススメのCrate

  • image-rs (image): 基本的な画像の入出力機能を提供します。
  • imageproc: フィルタリングやエッジ検出など、画像処理に特化した機能を提供します。imageクレートとセットで利用します。
  • rayon: データ処理を並列化するクレートです。

プログラミング例

画像処理を行うためのtraitを定義し、ユースケースに応じて処理を呼び出す方法を紹介します。以下にサンプルコードを示します。

transform.rs
use image::DynamicImage;
// 汎用transform trait
pub trait Transform {
    fn transform(&self, img: DynamicImage) -> DynamicImage;
}

pub struct Gaussian3 {}

impl Transform for Gaussian3 {
    fn transform(&self, img: DynamicImage) -> DynamicImage {
        let k = [
            1.0f32 / 16.0f32,
            2.0f32 / 16.0f32,
            1.0f32 / 16.0f32,
            2.0f32 / 16.0f32,
            4.0f32 / 16.0f32,
            2.0f32 / 16.0f32,
            1.0f32 / 16.0f32,
            2.0f32 / 16.0f32,
            1.0f32 / 16.0f32,
        ];
        img.filter3x3(&k)
    }
}
main.rs
pub mod transform;

use image::io::Reader as ImageReader;
use transform::Transform;

fn main() {
  let path = "test.png";
  let img = ImageReader::open(path).unwrap().decode().unwrap();
  let output_path = "test_out.png";

  let img2 = transform::Gaussian3{}.transform(img);
  img2.save(output_path).unwrap();
}

課題点

Bitmapの扱い

Rustのimageクレートでは、Bitmapにおけるピクセルフォーマットの扱いが複雑で、ピクセルフォーマットをコンパイル時に確定する GenericImage とピクセルフォーマットを実行時に確定する DynamicImage に分かれています。
ピクセルフォーマットに依存しない、またはフォーマットにより分岐する画像処理を書こうとした場合、imageクレートで利用しているマクロを使うか自力での実装が必要になる場合があります。この点は、技術的な課題として認識されていますが、めだった進展がないようです。

macro_rules! dynamic_map(
        ($dynimage: expr, $image: pat => $action: expr) => ({
            use DynamicImage::*;
            match $dynimage {
                ImageLuma8($image) => ImageLuma8($action),
                ImageLumaA8($image) => ImageLumaA8($action),
                ImageRgb8($image) => ImageRgb8($action),
                ImageRgba8($image) => ImageRgba8($action),
                ImageLuma16($image) => ImageLuma16($action),
                ImageLumaA16($image) => ImageLumaA16($action),
                ImageRgb16($image) => ImageRgb16($action),
                ImageRgba16($image) => ImageRgba16($action),
                ImageRgb32F($image) => ImageRgb32F($action),
                ImageRgba32F($image) => ImageRgba32F($action),
            }
        });

        ($dynimage: expr, |$image: pat| $action: expr) => (
            match $dynimage {
                DynamicImage::ImageLuma8($image) => $action,
                DynamicImage::ImageLumaA8($image) => $action,
                DynamicImage::ImageRgb8($image) => $action,
                DynamicImage::ImageRgba8($image) => $action,
                DynamicImage::ImageLuma16($image) => $action,
                DynamicImage::ImageLumaA16($image) => $action,
                DynamicImage::ImageRgb16($image) => $action,
                DynamicImage::ImageRgba16($image) => $action,
                DynamicImage::ImageRgb32F($image) => $action,
                DynamicImage::ImageRgba32F($image) => $action,
            }
        );
);

※ただしこのマクロは公開されていないため、自身のプログラムにコピーする必要があります。

パフォーマンスの問題

Pure Rustのクレートのみを使用すると、マルチプラットフォーム対応は容易ですが、例えばデコードやエンコード処理のパフォーマンスに問題が生じることがあります。これに対処するためには、プラットフォーム特化のクレートを使うか、自力での最適化が必要です。例えばWebAssemblyでの処理では、特にパフォーマンスの向上のためにJavaScriptのライブラリやHTML5 Canvasの利用を推奨します。

コンソールアプリケーションでは他言語で書かれたライブラリを利用するによりパフォーマンスの向上ができますが、サポートされるプラットフォームが限定されます。

まとめ

Rustでの画像処理は、そのマルチプラットフォーム対応や並列化の面で優れた選択肢です。ただし、Bitmapの扱いやパフォーマンスの問題には注意が必要です。この記事が、Rustを業務で使用する皆様の参考になれば幸いです。

Discussion