diff --git a/_assets/images/2017-03-01-Content-Aware-Image-Resize/e1i6pi.png b/_assets/images/2017-03-01-Content-Aware-Image-Resize/e1i6pi.png new file mode 100644 index 00000000..22295407 Binary files /dev/null and b/_assets/images/2017-03-01-Content-Aware-Image-Resize/e1i6pi.png differ diff --git a/_assets/images/2017-03-01-Content-Aware-Image-Resize/g1i.png b/_assets/images/2017-03-01-Content-Aware-Image-Resize/g1i.png new file mode 100644 index 00000000..7efd12a9 Binary files /dev/null and b/_assets/images/2017-03-01-Content-Aware-Image-Resize/g1i.png differ diff --git a/_assets/images/2017-03-01-Content-Aware-Image-Resize/g6j.png b/_assets/images/2017-03-01-Content-Aware-Image-Resize/g6j.png new file mode 100644 index 00000000..7bf72da1 Binary files /dev/null and b/_assets/images/2017-03-01-Content-Aware-Image-Resize/g6j.png differ diff --git a/_assets/images/2017-03-01-Content-Aware-Image-Resize/matrix1.png b/_assets/images/2017-03-01-Content-Aware-Image-Resize/matrix1.png new file mode 100644 index 00000000..13c17e3c Binary files /dev/null and b/_assets/images/2017-03-01-Content-Aware-Image-Resize/matrix1.png differ diff --git a/_assets/images/2017-03-01-Content-Aware-Image-Resize/matrix6.png.png b/_assets/images/2017-03-01-Content-Aware-Image-Resize/matrix6.png.png new file mode 100644 index 00000000..cf16a4d9 Binary files /dev/null and b/_assets/images/2017-03-01-Content-Aware-Image-Resize/matrix6.png.png differ diff --git a/_assets/images/2017-03-01-Content-Aware-Image-Resize/pp1p6.png b/_assets/images/2017-03-01-Content-Aware-Image-Resize/pp1p6.png new file mode 100644 index 00000000..3d1698ab Binary files /dev/null and b/_assets/images/2017-03-01-Content-Aware-Image-Resize/pp1p6.png differ diff --git a/_assets/images/2017-03-01-Content-Aware-Image-Resize/s6ig6i.png b/_assets/images/2017-03-01-Content-Aware-Image-Resize/s6ig6i.png new file mode 100644 index 00000000..4b087ee3 Binary files /dev/null and b/_assets/images/2017-03-01-Content-Aware-Image-Resize/s6ig6i.png differ diff --git a/_assets/images/2017-03-01-Content-Aware-Image-Resize/sample-image-200.jpeg b/_assets/images/2017-03-01-Content-Aware-Image-Resize/sample-image-200.jpeg new file mode 100644 index 00000000..11c700fc Binary files /dev/null and b/_assets/images/2017-03-01-Content-Aware-Image-Resize/sample-image-200.jpeg differ diff --git a/_assets/images/2017-03-01-Content-Aware-Image-Resize/sample-image-cropped.jpeg b/_assets/images/2017-03-01-Content-Aware-Image-Resize/sample-image-cropped.jpeg new file mode 100644 index 00000000..6f14fec7 Binary files /dev/null and b/_assets/images/2017-03-01-Content-Aware-Image-Resize/sample-image-cropped.jpeg differ diff --git a/_assets/images/2017-03-01-Content-Aware-Image-Resize/sample-image-gradient.jpeg b/_assets/images/2017-03-01-Content-Aware-Image-Resize/sample-image-gradient.jpeg new file mode 100644 index 00000000..92aeb940 Binary files /dev/null and b/_assets/images/2017-03-01-Content-Aware-Image-Resize/sample-image-gradient.jpeg differ diff --git a/_assets/images/2017-03-01-Content-Aware-Image-Resize/sample-image-paths.jpeg b/_assets/images/2017-03-01-Content-Aware-Image-Resize/sample-image-paths.jpeg new file mode 100644 index 00000000..386a4780 Binary files /dev/null and b/_assets/images/2017-03-01-Content-Aware-Image-Resize/sample-image-paths.jpeg differ diff --git a/_assets/images/2017-03-01-Content-Aware-Image-Resize/sample-image-yellow-path.jpeg b/_assets/images/2017-03-01-Content-Aware-Image-Resize/sample-image-yellow-path.jpeg new file mode 100644 index 00000000..f613aeb4 Binary files /dev/null and b/_assets/images/2017-03-01-Content-Aware-Image-Resize/sample-image-yellow-path.jpeg differ diff --git a/_assets/images/2017-03-01-Content-Aware-Image-Resize/sample-image.jpeg b/_assets/images/2017-03-01-Content-Aware-Image-Resize/sample-image.jpeg new file mode 100644 index 00000000..1797138b Binary files /dev/null and b/_assets/images/2017-03-01-Content-Aware-Image-Resize/sample-image.jpeg differ diff --git a/_posts/2017-03-01-Content-Aware-Image-Resize.md b/_posts/2017-03-01-Content-Aware-Image-Resize.md new file mode 100644 index 00000000..e990a836 --- /dev/null +++ b/_posts/2017-03-01-Content-Aware-Image-Resize.md @@ -0,0 +1,531 @@ +--- +title: "Изменение размера изображения с учётом содержимого" +categories: руководства +author: Martin Hafskjold Thoresen +original: https://mht.technology/post/content-aware-resize/ +translator: Gexon +--- + +# Изменение размера изображения с учётом содержимого +Изменение размера изображения с учётом содержимого (Content Aware Image Resize), +Жидкое растяжение (liquid resizing), ретаргетинг (retargeting) или вырезание шва +(seam carving), относятся к методу изменения размера изображения, где можно +вставлять или удалять *швы*, или наименее важные пути, для уменьшения или +наращивания изображения. Об этой идее я узнал из [ролика на YouTube](https://www.youtube.com/watch?v=qadw0BRKeMk), от Shai Avidan и Ariel Shamir. +В этой статье будет рассмотрена простая пробная реализация идеи изменения +размера изображения с учётом содержимого, естественно на языке Rust :) + +Для подопытной картинки, я поискал по запросу[[1]](#ref1) `"sample image"`, и нашел её[[2]](#ref2): + +{% img '2017-03-01-Content-Aware-Image-Resize/sample-image.jpeg' alt:'sample image' magick:resize:800 %} + +# Создаём макет согласно нисходящему подходу +Давайте начнем мозговой штурм. Думаю, наша библиотека может использоваться так: + +```rust +/// 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` могли бы быть такими: +```rust +/// 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`](https://crates.io/crates/image) от разработчиков “Piston” кажется подойдет, поэтому мы добавим в наш `Cargo.toml` запись: `image = "0.12"`. Быстрый поиск в документации это все, что требуется для того, чтобы написать функцию загрузки изображения: + +```rust +struct Image { + inner: image::DynamicImage, +} + +impl Image { + pub fn load_image(path: &Path) -> Image { + Image { + inner: image::open(path).unwrap() + } + } +} +``` + + + + +Естественно следующим шагом необходимо узнать как получить значение градиента из +`image::DynamicImage`. Контейнер image не может этого сделать, но у контейнера +[`imageproc`](https://crates.io/crates/imageproc) есть функция: `imageproc::gradients::sobel_gradients`. Однако нас поджидает небольшая проблема[[3]](#ref3). Функция `sobel_gradient` принимает 8-битное изображение в градациях серого, и возвращает 16-битное изображение в градациях серого. Изображение, которое мы загрузили - это изображение RGB с 8 битами на канал. Так что придется разложить каналы на R, G и B, преобразовать каждый канал в отдельные изображения в оттенках серого и вычислить градиенты каждого из них. А затем объединить градиенты вместе в одно изображение, в котором мы и будем искать путь. + +Это элегантно? Нет. Это будет работать? Возможно :) + +```rust +type GradientBuffer = image::ImageBuffer, Vec>; + +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` берёт наше изображение птицы и +возвращает это: + +{% img '2017-03-01-Content-Aware-Image-Resize/sample-image-gradient.jpeg' alt:'sample image gradient' magick:resize:800 %} + +# Путь наименьшего сопротивления +Теперь мы должны реализовать, пожалуй, самую сложную часть программы: DP - +алгоритм поиска пути наименьшего сопротивления. Давайте глянем как это будет +работать. Для простоты понимания, мы будем рассматривать только случай с поиском +вертикального пути. Представьте, что в таблице ниже это градиент изображения 6х6 +пикселей. + +{% img '2017-03-01-Content-Aware-Image-Resize/matrix1.png' alt:'просто матрица' magick:resize:800 %} + +Суть алгоритма состоит в поиске пути {% img '2017-03-01-Content-Aware-Image-Resize/pp1p6.png' magick:resize:800 %} +от одной из верхних ячеек {% img '2017-03-01-Content-Aware-Image-Resize/g1i.png' magick:resize:800 %} в одну из нижних {% img '2017-03-01-Content-Aware-Image-Resize/g6j.png' magick:resize:800 %}, так, чтобы минимизировать {% img '2017-03-01-Content-Aware-Image-Resize/e1i6pi.png' magick:resize:800 %}. Это может быть сделано путем создания новой таблицы S +используя следующее рекуррентное соотношение (без учета границы): + +{% img '2017-03-01-Content-Aware-Image-Resize/s6ig6i.png' magick:resize:800 %} + +То есть, каждая ячейка в таблице S это минимальная сумма от текущей ячейки до +самой нижней ячейки. Каждая ячейка выбирает одну из трех соседних ячеек, +расположенных строкой ниже, с наименьшим весом – это и будет следующей ячейкой +пути. Когда мы завершили заполнение таблицы S, мы просто выбираем наименьшее +число в самой верхней строке в качестве начальной ячейки. +Давайте найдем S: + +{% img '2017-03-01-Content-Aware-Image-Resize/matrix6.png.png' magick:resize:800 %} + +И вот оно! Мы видим, что есть путь, с суммой всех ячеек пути равной 8, и то, что +этот путь начинается в верхнем правом углу. Для того, чтобы найти путь, мы могли +бы запомнить, в какую сторону мы пошли для каждой ячейки (влево, вниз или +вправо), но нам это не нужно: мы просто выберем соседа снизу с наименьшим весом, +потому что значения веса клеток в таблице S указывают на кратчайший путь от +текущей ячейки к самой нижней. +Также обратите внимание, что есть два пути, которые в сумме дают 8 (у этих путей +различаются две нижние ячейки). + +# Реализация +Так-как мы пишем лишь макет программы, дальше мы сделаем по-простому. Мы +создадим структуру с нашей таблицей в виде массива и просто пройдемся по ней +циклом `for` согласно алгоритму. + +```rust +struct DPTable { + width: usize, + height: usize, + table: Vec, +} + +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. + +{% img '2017-03-01-Content-Aware-Image-Resize/sample-image-paths.jpeg' alt:'sample image paths' magick:resize:800 %} + +Эту картинку можно описать так: белые пиксели - это клетки, которые имеют +наибольший вес. Градиент этих пикселей более детализирован, что говорит о +высокой скорости изменений цвета (и именно эти участки картинки мы хотели бы +сохранить). +Поскольку алгоритм поиска пути будет искать наименьшие веса, которые +представлены здесь “более темными путями”, то алгоритм будет стараться избегать +светлых пикселей. То есть белые участки картинки. + +# Поиск пути +Теперь, когда у нас есть вся таблица, поиск лучшего пути не составит труда: это +- просто поиск из верхнего ряда и создания вектора индексов, всегда выбирая +самого маленького по весу соседа из нижней строки: + +```rust +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, +} + +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 штук, и покрасил жёлтым: + +{% img '2017-03-01-Content-Aware-Image-Resize/sample-image-yellow-path.jpeg' alt:'sample image yellow paths' magick:resize:800 %} + +По-моему, похоже на правду! + +# Удаление +Единственное, что осталось сейчас сделать - удалить пути, покрашенные жёлтым +цветом. Так как мы просто хотим сделать что-то работающее, мы можем сделать это +очень просто: возьмём сырые байты из нашей картинки, скопируем интервалы между +индексами, которые мы хотим удалить, в новый массив и создадим из него новое +изображение. + +```rust +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 строк: + +```rust +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); +} +``` + +{% img '2017-03-01-Content-Aware-Image-Resize/sample-image-cropped.jpeg' alt:'sample image cropped' magick:resize:800 %} + +Однако, мы видим, что алгоритм удалил многовато с правой стороны изображения, +хотя изображение более или менее уменьшено, это одна из проблем, которую надо +устранить! Быстрое и немного грязное исправление, чтобы просто немного изменить +градиент, путём явного задания границ на некоторое большое число, скажем 100. + +{% img '2017-03-01-Content-Aware-Image-Resize/sample-image-200.jpeg' alt:'sample image 200' magick:resize:800 %} + +Тадам! +Здесь немало косяков, что делает конечный результат немного менее +удовлетворительным. Однако птичка почти не пострадала и великолепно выглядит +(по-моему). Вы можете сказать, что мы уничтожили весь смысл композиции в +процессе уменьшения изображения. На это я скажу .... нуууу.... так-то да. + +# Увижу - поверю + +Сохранять изображения в файл и смотреть на них это прикольно, но это не +супер-крутое-изменение-размера-изображения-в-реальном-времени! Наконец, +попробуем собрать всё воедино. +Во-первых, нам необходима возможность загружать, получать и изменять размер +изображения за пределами контейнера. Мы постараемся сделать что-то вроде нашего +первоначального плана: + +```rust +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(); + // Так или иначе выведем эти данные в окно +} +``` + +Мы начнем с простого, добавив самое необходимое и по возможности следуя коротким +путем. + +```rust +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`. + +```rust +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(); + } +} +``` + +Вот и все. Один день работы, немного знаний по `sdl2`, `image`, и небольшой опыт +написания постов в блоге. +Надеюсь вам понравилось, хотя бы совсем немножко :) + +• [Git repository](https://www.github.com/martinhath/content-aware-resize) +• [/r/Rust thread](https://www.reddit.com/r/rust/comments/5ttzb4/implementing_content_aware_image_resizing/) +• [/r/Programming thread](https://www.reddit.com/r/programming/comments/5ttz9g/implementing_content_aware_image_resizing/) +• [HackerNews](https://news.ycombinator.com/item?id=13636706) +________________________________________ +1. Почему-то, duckduck-коед не работает, и гугль тоже, если +используется глагол. [[↑]](#ref1ret) +2. http://imgsv.imaging.nikon.com/lineup/lens/zoom/normalzoom/af-s_dx_18-140mmf_35-56g_ed_vr/img/sample/sample1_l.jpg [[↑]](#ref2ret) +3. Мне интересно, есть ли более простой способ! Кроме того, +сохранение результата градиента походу нереально, потому-что функция возвращает + `ImageBuffer` поверх `u16`, в то время как `ImageBuffer::save` требует, чтобы + основные данные были в `u8`. Я также не мог разобраться, как создать + `DynamicImage` (у которого также есть `a::save` с более понятным интерфейсом) + от `ImageBuffer`, ведь это возможно. [[↑]](#ref3ret) +