Tidy up the code for getting frames from animated GIFs
- ID
5a0c2e7- date
2021-11-29 23:19:56+00:00- author
Alex Chan <alex@alexwlchan.net>- parent
930acd7- message
Tidy up the code for getting frames from animated GIFs- changed files
2 files, 112 additions, 75 deletions
Changed files
src/get_bytes.rs (0) → src/get_bytes.rs (3825)
diff --git a/src/get_bytes.rs b/src/get_bytes.rs
new file mode 100644
index 0000000..430a9c4
--- /dev/null
+++ b/src/get_bytes.rs
@@ -0,0 +1,104 @@
+use std::fs::File;
+
+use image::{AnimationDecoder, Frame, DynamicImage};
+use image::codecs::gif::GifDecoder;
+use image::imageops::FilterType;
+
+pub fn get_bytes_for_image(path: &str) -> Vec<u8> {
+ let img = match image::open(&path) {
+ Ok(im) => im,
+ Err(e) => {
+ eprintln!("{}", e);
+ std::process::exit(1);
+ },
+ };
+
+ // Resize the image after we open it. For this tool I'd rather get a good answer
+ // quickly than a great answer slower.
+ //
+ // The choice of max dimension is arbitrary. Making it smaller means you get
+ // faster results, but possibly at the loss of quality.
+ //
+ // The nearest neighbour algorithm produces images that don't look as good,
+ // but it's much much faster and the loss of quality is unlikely to be
+ // an issue when looking for dominant colours.
+ //
+ // Note: when trying to work out what's "fast enough", make sure you use release
+ // mode. The image/k-means operations are significantly faster (=2 orders
+ // of magnitude) than in debug mode.
+ //
+ // See https://docs.rs/image/0.23.14/image/imageops/enum.FilterType.html
+ // println!("{:?}", img);
+ let resized_img = img.resize(400, 400, FilterType::Nearest);
+
+ resized_img.into_rgba8().into_raw()
+}
+
+pub fn get_bytes_for_gif(path: &str) -> Vec<u8> {
+ let f = File::open(path).unwrap();
+ let decoder = GifDecoder::new(f).ok().unwrap();
+
+ // If the GIF is animated, we want to make sure we look at multiple
+ // frames when choosing the dominant colour.
+ //
+ // We don't want to pass all the frames to the k-means analysis, because
+ // that would be incredibly memory-intensive and is unnecessary -- see
+ // previous comments about wanting a good enough answer quickly.
+ //
+ // For that reason, we select a sample of up to 50 frames and use those
+ // as the basis for analysis.
+ let frames: Vec<Frame> =
+ decoder
+ .into_frames()
+ .collect_frames()
+ .unwrap();
+
+ // How this works: it tells us we should be looking at the nth frame.
+ // Examples:
+ //
+ // frame count | nth frame | comment
+ // ------------+-----------+---------
+ // 1 | 1 | in a 1-frame GIF, look at the only frame
+ // 25 | 1 | look at every frame
+ // 50 | 2 | look at every second frame
+ // 78 | 3 | look at every third frame
+ //
+ // I'm sure there's a more idiomatic way to do this, but it was late
+ // when I wrote this and it seems to work.
+ //
+ let nth_frame = if frames.len() <= 50 {
+ 1
+ } else {
+ ((frames.len() as f32) / (25 as f32)) as i32
+ };
+
+ let selected_frames =
+ frames
+ .iter()
+ .enumerate()
+ .filter(|(i, _)| {
+ (*i as f32 / nth_frame as f32).floor() == (*i as f32 / nth_frame as f32)
+ })
+ .map(|(_, frame)| frame);
+
+ // Now we go through the frames and extract all the pixels. The k-means
+ // process doesn't care about position, so we can concatenate the pixels
+ // for each frame into one big Vec.
+ //
+ // As with non-GIF images, we resize the images down before loading them.
+ // We resize to a smaller frame in GIFs because if there are multiple
+ // frames, we don't care as much about individual frames, and we want
+ // to avoid a large Vec<u8> in memory.
+ let resize = if frames.len() == 1 { 400 } else { 100 };
+
+ selected_frames
+ .map(|frame|
+ DynamicImage::ImageRgba8(frame.buffer().clone())
+ .resize(resize, resize, FilterType::Nearest)
+ .into_rgba8()
+ .into_raw()
+ )
+ .into_iter()
+ .flatten()
+ .collect()
+}
src/main.rs (10128) → src/main.rs (7614)
diff --git a/src/main.rs b/src/main.rs
index 3b4977c..19b34be 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,17 +1,13 @@
-// #![deny(warnings)]
+#![deny(warnings)]
#[macro_use]
extern crate clap;
-use std::fs::File;
-
use clap::{App, Arg};
-use image::imageops::FilterType;
+use kmeans_colors::get_kmeans_hamerly;
use palette::{Lab, Pixel, Srgb, Srgba};
-use kmeans_colors::{get_kmeans_hamerly};
-use image::codecs::gif::GifDecoder;
-use image::AnimationDecoder;
-use image::{Frames, ImageBuffer, DynamicImage};
+
+mod get_bytes;
const VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -49,79 +45,16 @@ fn main() {
// See https://github.com/clap-rs/clap/blob/v2.33.1/examples/12_typed_values.rs
let max_colours = value_t!(matches, "MAX-COLOURS", usize).unwrap_or_else(|e| e.exit());
- let decoder = GifDecoder::new(File::open(&path).unwrap()).ok().unwrap();
- let frames2: Frames = decoder.into_frames();
- let frames3 = frames2.collect_frames().unwrap();
-
- let increment = if frames3.len() <= 50 {
- 1
+ let img_bytes = if path.ends_with(".gif") {
+ get_bytes::get_bytes_for_gif(path)
} else {
- let s: f32 = (frames3.len() as f32) / (25 as f32);
- s.floor() as i32
- };
-
- let buffers: Vec<Vec<u8>> = frames3.iter()
- .enumerate()
- .filter(|(i, _)| {
- (*i as f32 / increment as f32).floor() == (*i as f32 / increment as f32)
- }
-
- )
- .map(|(i, frame)| DynamicImage::ImageRgba8(frame.buffer().clone()).resize(100, 100, FilterType::Nearest).into_rgba8().into_raw())
- .collect();
-
- println!("incrmenet = {:?}", increment);
- println!("frame count = {:?}", frames3.len());
- println!("images count = {:?}", buffers.len());
-
- // let buffers: Vec<Vec<u8>> = images.iter().map(|img| img).collect();
-
- // std::process::exit(1);
- //
- // let mut frames = GifDecoder::new(File::open(&path).unwrap()).ok().unwrap()
- // .into_frames().collect_frames().unwrap();
- // frames.truncate(25);
- //
- // println!("frame count = {:?}", frames.len());
-
- // let buffers: Vec<Vec<u8>> = frames.iter().map(|f| f.to_owned().into_buffer().into_raw()).collect();
- let bytes: Vec<u8> = buffers.into_iter().flatten().collect();
-
- println!("pixels = {:?}", bytes.len());
-
- let img = match image::open(&path) {
- Ok(im) => im,
- Err(e) => {
- eprintln!("{}", e);
- std::process::exit(1);
- },
+ get_bytes::get_bytes_for_image(path)
};
- // Resize the image after we open it. For this tool I'd rather get a good answer
- // quickly than a great answer slower.
- //
- // The choice of max dimension is arbitrary. Making it smaller means you get
- // faster results, but possibly at the loss of quality.
- //
- // The nearest neighbour algorithm produces images that don't look as good,
- // but it's much much faster and the loss of quality is unlikely to be
- // an issue when looking for dominant colours.
- //
- // Note: when trying to work out what's "fast enough", make sure you use release
- // mode. The image/k-means operations are significantly faster (=2 orders
- // of magnitude) than in debug mode.
- //
- // See https://docs.rs/image/0.23.14/image/imageops/enum.FilterType.html
- // println!("{:?}", img);
- let resized_img = img.resize(400, 400, FilterType::Nearest);
- println!("{}", path);
-
- let img_vec: Vec<u8> = resized_img.into_rgba8().into_raw();
-
// This is based on code from the kmeans-colors binary, but with a bunch of
// the options stripped out.
// See https://github.com/okaneco/kmeans-colors/blob/9960c55dbc572e08d564dc341d6fd7e66fa79b5e/src/bin/kmeans_colors/app.rs
- let lab: Vec<Lab> = Srgba::from_raw_slice(&bytes)
+ let lab: Vec<Lab> = Srgba::from_raw_slice(&img_bytes)
.iter()
.map(|x| x.into_format().into())
.collect();