動機
仕事で現場の写真をたくさん撮影しますが、うちの課に割り当てられているストレージが 40GB しかないので、すぐに容量一杯まで使いきってしまいます。Rust で jpeg をリサイズして mozjpeg で高圧縮化(その2)は画像ファイルサイズをできるだけ小さくすることを目指した結果を書き留めたものです。今回は OpenCV のノイズ除去を利用してファイルサイズをさらに小さくしようという試みです。
環境
Windows 10 64bit
rustc 1.46.0
OpenCV 4.3.0
LLVM 10.0.0
準備
Windows 環境なら chocolatey 等を使えば良いようですが、会社の Auth Proxy 下では使えなかったので、手動で環境を準備しました。
LLVM のインストール
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 から 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 でのデコード、エンコードは先の記事を参照してください。ここでは mozjpeg でデコードしたデータを OpenCV に渡して、また mozjpeg に戻すとこまで書きます。
// 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
を可変にした方が良いかもしれません。
コピペ用サンプルコード
[package]
name = "denoise"
version = "0.1.0"
authors = ["benki"]
edition = "2018"
[dependencies]
anyhow = "1.0"
mozjpeg = "0.8"
opencv = "0.45"
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(())
}
結果
ファイルサイズが10分の1以下になりました。やったぜ
ノイズ除去でちょっとのっぺりした画像になってディテールが一部つぶれていますね。それが気になる場合は、fast_nl_means_denoising_colored
の3番目、4番目の引数を小さくして(3.0 → 2.0)様子を見ると良いと思います。
以上、機械メーカのおじさんが現場からお伝えしました。