コンテンツに合わせて画像のサイズを変更する

画像のサイズ変更、コンテンツに応じた画像のサイズ変更、液体のサイズ変更、ターゲット変更、またはシームカービングは、画像のサイズを変更する方法です。画像を縮小または拡大します。 このアイデアについては、Shai AvidanとAriel ShamirのYouTubeビデオから学びました。







この記事では、Rustでコンテンツに合わせて画像のサイズを変更するというアイデアの簡単な試行実装について説明します。







実験画像については、 1つの "sample image"



を検索し、 2を見つけました。







画像









トップダウンのアプローチに従ってレイアウトを作成します



ブレーンストーミングセッションを始めましょう。 ライブラリは次のように使用できると思います。







 /// caller.rs let mut image = car::load_image(path); //   ? image.resize_to(car::Dimensions::Absolute(800, 580)); //  20 ? image.resize_to(car::Dimensions::Relative(0, -20)); //    ? car::show_image(&image); //    ? image.save("resized.jpeg");
      
      





lib.rs



の最も重要な関数はlib.rs



です。







 /// lib.rs pub fn load_image(path: Path) -> Image { //      :) Image { inner: some_image_lib::load(path).unwrap(), } } impl Image { pub fn resize_to(&mut self, dimens: Dimensions) { //     /? let (mut xs, mut ys) = self.size_diffs(dimens); //     , //       , //      . while xs != 0 && ys != 0 { let best_horizontal = image.best_horizontal_path(); let best_vertical = image.best_vertical_path(); //     . if best_horizontal.score < best_vertical.score { self.handle_path(best_horizontal, &mut xs); } else { self.handle_path(best_vertical, &mut ys); } } //    . while xs != 0 { let path = image.best_horizontal_path(); self.handle_path(path, &mut xs); } while ys != 0 { let path = image.best_vertical_path(); self.handle_path(path, &mut ys); } } }
      
      





これにより、システム作成のアプローチ方法に関する洞察が得られます。 写真をアップロードし、これらの継ぎ目またはパスを見つけ、画像からそのパスを削除する必要があります。 さらに、結果を見たいと思います。 最初に画像をアップロードしましょう。 使用するAPIは既にわかっています。







画像



「ピストン」開発者からのimage



ライブラリが適切であると思われるため、エントリを追加します: image = "0.12"



Cargo.toml



ます。 画像アップロード機能を作成するために必要なのは、ドキュメント内のクイック検索だけです。







 struct Image { inner: image::DynamicImage, } impl Image { pub fn load_image(path: &Path) -> Image { Image { inner: image::open(path).unwrap() } } }
      
      





当然、次のステップは、勾配値を取得する方法を見つけることです

image::DynamicImage



イメージコンテナーはこれを行うことができませんが、 imageproc



コンテナーには関数: imageproc::gradients::sobel_gradients



ます。 しかし、小さな問題が待っています3sobel_gradient



関数は、8ビットのグレースケール画像を受け入れ、16ビットのグレースケール画像を返します。 アップロードした画像は、チャンネルあたり8ビットのRGB画像です。 そのため、チャンネルをR、G、Bに分解し、各チャンネルを個別のグレースケール画像に変換し、それぞれのグラデーションを計算する必要があります。 そして、グラデーションを1つの画像にまとめて、パスを探します。







エレガントですか? いや これは機能しますか? おそらく。







 type GradientBuffer = image::ImageBuffer<image::Luma<u16>, Vec<u16>>; impl Image { pub fn load_image(path: &Path) -> Image { Image { inner: image::open(path).unwrap() } } fn gradient_magnitude(&self) -> GradientBuffer { //   RGB let (red, green, blue) = decompose(&self.inner); let r_grad = imageproc::gradients::sobel_gradients(red.as_luma8().unwrap()); let g_grad = imageproc::gradients::sobel_gradients(green.as_luma8().unwrap()); let b_grad = imageproc::gradients::sobel_gradients(blue.as_luma8().unwrap()); let (w, h) = r_grad.dimensions(); let mut container = Vec::with_capacity((w * h) as usize); for (r, g, b) in izip!(r_grad.pixels(), g_grad.pixels(), b_grad.pixels()) { container.push(r[0] + g[0] + b[0]); } image::ImageBuffer::from_raw(w, h, container).unwrap() } } fn decompose(image: &image::DynamicImage) -> (image::DynamicImage, image::DynamicImage, image::DynamicImage) { let w = image.width(); let h = image.height(); let mut red = image::DynamicImage::new_luma8(w, h); let mut green = image::DynamicImage::new_luma8(w, h); let mut blue = image::DynamicImage::new_luma8(w, h); for (x, y, pixel) in image.pixels() { let r = pixel[0]; let g = pixel[1]; let b = pixel[2]; red.put_pixel(x, y, *image::Rgba::from_slice(&[r, r, r, 255])); green.put_pixel(x, y, *image::Rgba::from_slice(&[g, g, g, 255])); blue.put_pixel(x, y, *image::Rgba::from_slice(&[b, b, b, 255])); } (red, green, blue) }
      
      





開始後、 Image::gradient_magnitune



は鳥の画像を取得し、これを返します:







画像







最小抵抗の経路



ここで、おそらくプログラムの最も難しい部分を実装する必要があります。DP-抵抗が最小のパスを見つけるためのアルゴリズム。 これがどのように機能するかを見てみましょう。 理解を容易にするために、垂直パスを検索する場合のみを考慮します。 次の表に、6x6ピクセルの画像グラデーションがあることを想像してください。













アルゴリズムの本質は、パスを見つけることです 上のセルの1つから 底の一つに 最小化するために 。 これは、次の繰り返し関係(境界線を除く)を使用して新しいテーブルSを作成することで実行できます。













つまり、テーブルSの各セルは、現在のセルから最下位のセルまでの最小量です。 各セルは、下の行にある3つの隣接セルのうち、重みが最小のセルの1つを選択します。これがパスの次のセルになります。 テーブルSの入力が完了したら、一番上の行の最小の数字を開始セルとして選択します。







Sを見つけましょう:

























そしてここにある! パスのすべてのセルの合計が8に等しいパスがあり、このパスが右上隅から始まることがわかります。 方法を見つけるために、各セルに行った方法(左、下、または右)を覚えることができますが、それは必要ではありません。下のセルの重み値が最も低い隣のものを選択するだけです。 Sは、現在のセルから最下位までの最短パスを示します。 また、合計で8つのパスが2つあることに注意してください(これらのパスには2つの下位セルがあります)。







実装



したがって、プログラムレイアウトを記述するだけなので、簡単な方法でそれを行います。 配列の形式でテーブルを使用して構造体を作成し、アルゴリズムに従ってfor



ループを使用して構造体を調べます。







 struct DPTable { width: usize, height: usize, table: Vec<u16>, } impl DPTable { fn from_gradient_buffer(gradient: &GradientBuffer) -> Self { let dims = gradient.dimensions(); let w = dims.0 as usize; let h = dims.1 as usize; let mut table = DPTable { width: w, height: h, table: vec![0; w * h], }; //  gradient[h][w],       let get = |w, h| gradient.get_pixel(w as u32, h as u32)[0]; //     for i in 0..w { let px = get(i, h - 1); table.set(i, h - 1, px) } //      J,      //  .        for row in (0..h - 1).rev() { for col in 1..w - 1 { let l = table.get(col - 1, row + 1); let m = table.get(col , row + 1); let r = table.get(col + 1, row + 1); table.set(col, row, get(col, row) + min(min(l, m), r)); } //        : let left = get(0, row) + min(table.get(0, row + 1), table.get(1, row + 1)); table.set(0, row, left); let right = get(0, row) + min(table.get(w - 1, row + 1), table.get(w - 2, row + 1)); table.set(w - 1, row, right); } table } }
      
      





開始後、 DPTable



テーブルをGradientBuffer



に戻し、ファイルに書き込むことができます。 以下の画像のピクセルは、パスの重みを128で割ったものです。







画像







この画像は、次のように説明できます。白いピクセルは、最大の重みを持つセルです。 これらのピクセルの勾配はより詳細であり、これは色の変化が高速であることを示しています(画像のこれらのセクションが保持することになるでしょう)。







パス検索アルゴリズムは、ここで「暗いパス」として表示される最小の重みを探すため、アルゴリズムは明るいピクセルを避けようとします。 つまり、画像の白い領域。







道を探す



テーブル全体ができたので、最良の方法を見つけるのは簡単です。これは、一番上の行から検索してインデックスベクトルを作成し、常に一番下の行から重みで最小の近傍を選択するだけです。







 impl DPTable { fn path_start_index(&self) -> usize { //     ?! //     . self.table.iter() .take(self.width) .enumerate() .map(|(i, n)| (n, i)) .min() .map(|(_, i)| i) .unwrap() } } struct Path { indices: Vec<usize>, } impl Path { pub fn from_dp_table(table: &DPTable) -> Self { let mut v = Vec::with_capacity(table.height); let mut col: usize = table.path_start_index(); v.push(col); for row in 1..table.height { //  ,    . if col == 0 { let m = table.get(col, row); let r = table.get(col + 1, row); if m > r { col += 1; } //  ,     } else if col == table.width - 1 { let l = table.get(col - 1, row); let m = table.get(col, row); if l < m { col -= 1; } } else { let l = table.get(col - 1, row); let m = table.get(col, row); let r = table.get(col + 1, row); let minimum = min(min(l, m), r); if minimum == l { col -= 1; } else if minimum == r { col += 1; } } v.push(col + row * table.width); } Path { indices: v } } }
      
      





選択したパスが多かれ少なかれもっともらしいことを確認するために、それらを生成しました

10個、黄色に塗装:







画像







私の意見では、それは本当のようです!







削除する



あとは、黄色のパスを削除するだけです。 何かを機能させたいだけなので、非常に簡単に行うことができます。写真から生のバイトを取り出し、削除したいインデックス間の間隔を新しい配列にコピーし、そこから新しい画像を作成します。







 impl Image { fn remove_path(&mut self, path: Path) { let image_buffer = self.inner.to_rgb(); let (w, h) = image_buffer.dimensions(); let container = image_buffer.into_raw(); let mut new_pixels = vec![]; let mut path = path.indices.iter(); let mut i = 0; while let Some(&index) = path.next() { new_pixels.extend(&container[i..index * 3]); i = (index + 1) * 3; } new_pixels.extend(&container[i..]); let ib = image::ImageBuffer::from_raw(w - 1, h, new_pixels).unwrap(); self.inner = image::DynamicImage::ImageRgb8(ib); } }
      
      





最後に、時が来ました。 これで、画像から行を削除するか、この関数をループで呼び出して、たとえば200行を削除できます。







 let mut image = Image::load_image(path::Path::new("sample-image.jpg")); for _ in 0..200 { let grad = image.gradient_magnitude(); let table = DPTable::from_gradient_buffer(&grad); let path = Path::from_dp_table(&table); image.remove_path(path); }
      
      





画像







ただし、アルゴリズムによって画像の右側が少し削除されすぎていることがわかります。画像は多少縮小されていますが、これは修正が必要な問題の1つです。 迅速でわずかに汚い修正方法は、境界をいくつかの大きな数値、たとえば100に明示的に設定することにより、勾配を少し変更することです。







画像







タダム!







在庫がたくさんあるため、最終結果は少し満足できません。 しかし、鳥はほとんど苦しんでおらず、見た目も素晴らしい(私の意見では)。 画像を縮小する過程で、構図の意味全体を破壊したと言えます。 これに私は言う...まあ...まあ、はい。







見る-信じる



画像をファイルに保存して見るのは楽しいですが、リアルタイムで超クールなサイズ変更画像ではありません! 最後に、すべてをまとめてみてください。







まず、コンテナの外部で画像をアップロード、受信、サイズ変更する機能が必要です。 元の計画のようなことをしようとします。







 extern crate content_aware_resize; use content_aware_resize as car; fn main() { let mut image = car::load_image(path); image.resize_to(car::Dimensions::Relative(-1, 0)); let data: &[u8] = image.get_image_data(); //         }
      
      





シンプルなものから始め、最も必要なものを追加し、可能であれば短い道をたどります。







 pub enum Dimensions { Relative(isize, isize), } ... impl Image { fn size_difference(&self, dims: Dimensions) -> (isize, isize) { let (w, h) = self.inner.dimensions(); match dims { Dimensions::Relative(x, y) => { (w as isize + x, h as isize + x) } } } pub fn resize_to(&mut self, dimensions: Dimensions) { let (mut xs, mut _ys) = self.size_difference(dimensions); //      if xs < 0 { panic!("Only downsizing is supported.") } if _ys != 0 { panic!("Only horizontal resizing is supported.") } while xs > 0 { let grad = self.gradient_magnitude(); let table = DPTable::from_gradient_buffer(&grad); let path = Path::from_dp_table(&table); self.remove_path(path); xs -= 1; } } pub fn get_image_data(&self) -> &[u8] { self.inner.as_rgb8().unwrap() } }
      
      





ほんの少しコピー&ペースト!







さて、おそらくサイズ変更可能なウィンドウが必要です。 sdl2



コンテナを使用して、新しいプロジェクトをすばやく展開できます。







 extern crate content_aware_resize; extern crate sdl2; use content_aware_resize as car; use sdl2::rect::Rect; use sdl2::event::{Event, WindowEvent}; use sdl2::keyboard::Keycode; use std::path::Path; fn main() { //   let mut image = car::Image::load_image(Path::new("sample-image.jpeg")); let (mut w, h) = image.dimmensions(); //  sdl2    let sdl_ctx = sdl2::init().unwrap(); let video = sdl_ctx.video().unwrap(); let window = video.window("Context Aware Resize", w, h) .position_centered() .opengl() .resizable() .build() .unwrap(); let mut renderer = window.renderer().build().unwrap(); //    ""     let update_texture = |renderer: &mut sdl2::render::Renderer, image: &car::Image| { let (w, h) = image.dimmensions(); let pixel_format = sdl2::pixels::PixelFormatEnum::RGB24; let mut tex = renderer.create_texture_static(pixel_format, w, h).unwrap(); let data = image.get_image_data(); let pitch = w * 3; tex.update(None, data, pitch as usize).unwrap(); tex }; let mut texture = update_texture(&mut renderer, &image); let mut event_pump = sdl_ctx.event_pump().unwrap(); 'running: loop { for event in event_pump.poll_iter() { //       match event { Event::Quit {..} | Event::KeyDown { keycode: Some(Keycode::Escape), .. } => { break 'running }, Event::Window {win_event: WindowEvent::Resized(new_w, _h), .. } => { //       , //       let x_diff = new_w as isize - w as isize; if x_diff < 0 { image.resize_to(car::Dimensions::Relative(x_diff, 0)); } w = new_w as u32; texture = update_texture(&mut renderer, &image); }, _ => {} } } // ,   . renderer.clear(); renderer.copy(&texture, None, Some(Rect::new(0, 0, w, h))).unwrap(); renderer.present(); } }
      
      





以上です。 仕事の1日、 sdl2



image



について少し知識がsdl2



、ブログ投稿を書く少しの経験。







少なくとも少し楽しんでいただけたでしょうか。












  1. 何らかの理由で、duckduck-koedは機能せず、動詞が使用されている場合はGoogleも機能しません。 [↑]
  2. http://imgsv.imaging.nikon.com/lineup/lens/zoom/normalzoom/af-s_dx_18-140mmf_35-56g_ed_vr/img/sample/sample1_l.jpg [↑]
  3. もっと簡単な方法があるかどうか疑問に思っています! また

    関数が返すため、勾配の結果を保存することは非現実的です

    ImageBuffer



    ImageBuffer::save



    が必要とする一方で、 u16



    上のu16





    主なデータはu8



    。 私も作成する方法を理解できませんでした

    DynamicImage



    (より直感的なインターフェイスでa::save



    することもできます)

    ImageBuffer



    から、それは可能です。 [↑]


翻訳者のメモ



ロシア語を話すラスタマンruRustのコミュニティ全体に感謝します。

以下に感謝します:










All Articles