Rust で画像のノイズ除去（OpenCV）

## 動機
仕事で現場の写真をたくさん撮影しますが、うちの課に割り当てられているストレージが 40GB しかないので、すぐに容量一杯まで使いきってしまいます。[Rust で jpeg をリサイズして mozjpeg で高圧縮化（その2）](https://qiita.com/benki/items/b12892fea32e50d2ba77)は画像ファイルサイズをできるだけ小さくすることを目指した結果を書き留めたものです。今回は OpenCV のノイズ除去を利用してファイルサイズをさらに小さくしようという試みです。

## 環境
Windows 10 64bit
rustc 1.46.0
OpenCV 4.3.0
LLVM 10.0.0

## 準備
[Windows 環境なら chocolatey 等を使えば良い](https://github.com/twistedfall/opencv-rust#windows-package)ようですが、会社の Auth Proxy 下では使えなかったので、手動で環境を準備しました。

### LLVM のインストール
[https://releases.llvm.org/download.html#10.0.0](https://releases.llvm.org/download.html#10.0.0) から LLVM-10.0.0-win64.exe をダウンロードしてインストール。「Path に LLVM を追加」をチェックすること。

### OpenCV のインストール
[https://github.com/opencv/opencv/releases/tag/4.3.0](https://github.com/opencv/opencv/releases/tag/4.3.0) から opencv-4.3.0-vc14_vc15.exe をダウンロードして解凍。```C:¥opencv```に保存。

以下の通り環境変数の設定。

```
PATH=$PATH;C:¥opencv¥build¥x64¥vc15¥bin
OPENCV_LINK_LIBS=opencv_world430
OPENCV_LINK_PATHS=C:¥opencv¥build¥x64¥vc15¥lib
OPENCV_INCLUDE_PATHS=C:¥opencv¥build¥include
```

## 実装
mozjpeg でのデコード、エンコードは[先の記事](https://qiita.com/benki/items/b12892fea32e50d2ba77)を参照してください。ここでは mozjpeg でデコードしたデータを OpenCV に渡して、また mozjpeg に戻すとこまで書きます。

```rust
// mozjpeg でデコードした data: Vec<u8> を OpenCV の Mat に変換
let src = unsafe {
    Mat::new_rows_cols_with_data(
        height as i32,
        width as i32,
        CV_8UC3,
        data.as_mut_ptr() as *mut c_void,
        Mat_AUTO_STEP
    )?
};

// OpenCV で処理したデータを保存するための Mat を用意
let mut dst = Mat::default()?;

// ノイズ除去処理
fast_nl_means_denoising_colored(&src, &mut dst, 3.0, 3.0, 7, 21)?;

// OpenCV の処理結果を Vec<u8> に変換
let data: Vec<u8> = dst
        .data_typed::<Vec3b>()?
        .iter()
        .map(|v| &v[..])
        .flatten()
        .cloned()
        .collect();
```

```fast_nl_means_denoising_colored```の引数について推奨値は```(..., 3.0, 3.0, 7, 21)```ですが、画像のピクセルサイズやどのくらい強さでノイズ除去するかを調整しながら増減した方が良いです。特に最後の引数の```search_window_size```は処理速度に影響してきます。画像のサイズが大きいときは現実的な時間で処理が終わらないということになるので、画像サイズによって```search_window_size```を可変にした方が良いかもしれません。

<details><summary>コピペ用サンプルコード</summary><div>

```toml:Cargo.toml
[package]
name = "denoise"
version = "0.1.0"
authors = ["benki"]
edition = "2018"

[dependencies]
anyhow = "1.0"
mozjpeg = "0.8"
opencv = "0.45"
```

```rust:main.rs
use anyhow::{anyhow, Result};
use mozjpeg::{ColorSpace, Compress, Decompress, Marker, ScanMode, ALL_MARKERS};
use opencv::{
    core::{Mat, Vec3b, CV_8UC3, Mat_AUTO_STEP},
    prelude::*,
    photo::fast_nl_means_denoising_colored,
};
use std::ffi::c_void;
use std::fs;

fn main() -> Result<()> {
    let raw_data = fs::read("C:\\Users\\benki\\Downloads\\0.jpg")?;
    let decomp = Decompress::with_markers(ALL_MARKERS).from_mem(&raw_data)?;

    // markers の中に Exif 情報がある
    let markers: Vec<(Marker, Vec<u8>)> = decomp
        .markers()
        .into_iter()
        .map(|m| (m.marker, m.data.to_owned()))
        .collect();

    // RGB 形式でデコード開始
    let mut decomp_started = decomp.rgb()?;

    // 幅・高さ取得
    let width = decomp_started.width();
    let height = decomp_started.height();

    // デコードされたデータの取得
    let mut data: Vec<u8> = decomp_started
        .read_scanlines::<[u8; 3]>()
        .ok_or(anyhow!("read_scanlines error"))?
        .iter()
        .flatten()
        .cloned()
        .collect();

    // デコードの終了処理
    decomp_started.finish_decompress();

    // mozjpeg でデコードした data: Vec<u8> を OpenCV の Mat に変換
    let src = unsafe {
        Mat::new_rows_cols_with_data(
            height as i32,
            width as i32,
            CV_8UC3,
            data.as_mut_ptr() as *mut c_void,
            Mat_AUTO_STEP
        )?
    };

    // OpenCV で処理したデータを保存するための Mat を用意
    let mut dst = Mat::default()?;

    // ノイズ除去処理
    fast_nl_means_denoising_colored(&src, &mut dst, 3.0, 3.0, 7, 21)?;

    // OpenCV の処理結果を Vec<u8> に変換
    let data: Vec<u8> = dst
            .data_typed::<Vec3b>()?
            .iter()
            .map(|v| &v[..])
            .flatten()
            .cloned()
            .collect();
    
    // mozjpeg での圧縮処理
    let mut comp = Compress::new(ColorSpace::JCS_RGB);
    comp.set_scan_optimization_mode(ScanMode::AllComponentsTogether);
    comp.set_quality(70.0);
    comp.set_size(width, height);
    comp.set_mem_dest();
    comp.start_compress();

    // Exif 情報を書き込む
    markers.into_iter().for_each(|m| {
        comp.write_marker(m.0, &m.1);
    });

    // RGB データを書き込む
    let mut line = 0;
    loop {
        if line > height - 1 {
            break;
        }
        let buf = unsafe { data.get_unchecked(line * width * 3..(line + 1) * width * 3) };
        comp.write_scanlines(buf);
        line += 1;
    }

    // 圧縮の終了処理
    comp.finish_compress();

    // ファイルに保存
    let buf = comp.data_to_vec().map_err(|e| anyhow!("{:?}", e))?;
    fs::write("C:\\Users\\benki\\Downloads\\1.jpg", &buf)?;
    Ok(())
}
```
</div></details>

## 結果
オリジナル画像（751KB）
![0.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353186/25c1784d-5356-7663-8a54-81f7dea539db.jpeg)

ノイズ除去 + mozjpegで圧縮後（62KB）
![1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/353186/5390daea-ab74-325b-773c-b0e4cd0e9980.jpeg)

ファイルサイズが10分の1以下になりました。やったぜ:relaxed:

ノイズ除去でちょっとのっぺりした画像になってディテールが一部つぶれていますね。それが気になる場合は、```fast_nl_means_denoising_colored```の3番目、4番目の引数を小さくして（3.0 → 2.0）様子を見ると良いと思います。

以上、機械メーカのおじさんが現場からお伝えしました。