8 Commits
1.1.0 ... 1.2.0

Author SHA1 Message Date
763c82a89e video path added 2024-07-08 18:28:05 +02:00
91051a82be video playback test 2024-07-08 14:34:57 +02:00
99f7fa2a28 ascii-gen -> ascii 2024-07-08 00:00:03 +02:00
a75c300692 camera read 2024-07-07 23:58:15 +02:00
2e87792a7f stdin read 2024-07-07 15:41:53 +02:00
e0f043bb77 more params 2024-07-07 14:42:58 +02:00
274ceb0b83 optimized code a bit 2024-07-07 14:13:23 +02:00
0d7e950503 invert color 2024-07-07 13:51:40 +02:00
11 changed files with 1725 additions and 152 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/target /target
*.png *.png
*.webm

1268
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,14 @@
[package] [package]
name = "ascii-gen" name = "ascii"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
atty = "0.2.14"
clap = { version = "4.5.8", features = ["derive"] } clap = { version = "4.5.8", features = ["derive"] }
clearscreen = "3.0.0"
colored = "2.1.0" colored = "2.1.0"
ffmpeg-next = "7.0.2"
image = "0.24.9" image = "0.24.9"
nokhwa = { path = "../../Software/nokhwa", version = "0.10.4", features = ["input-native"] }
video-rs = "0.8.1"

View File

@@ -1,3 +1,6 @@
pub mod args; pub mod args;
pub mod image; pub mod image;
pub mod ascii; pub mod ascii;
pub mod gen_handler;
pub mod cam_handler;
pub mod vid_handler;

View File

@@ -1,23 +1,107 @@
use clap::Parser; use clap::{Parser, Subcommand};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
pub struct Args { pub struct Args {
#[arg(short, long)] #[command(subcommand)]
pub image: String, pub command: Option<Subcommands>,
}
#[arg(long, default_value_t = false)] #[derive(Subcommand, Debug)]
pub invert: bool, pub enum Subcommands {
Gen {
#[arg(short, long, default_value_t = String::from(""))]
image: String,
#[arg(long, default_value_t = false)] #[arg(long, default_value_t = false)]
pub colorful: bool, invert: bool,
#[arg(long, default_value_t = 80)] #[arg(long, default_value_t = false)]
pub width: usize, colorful: bool,
#[arg(long, default_value_t = 25)] #[arg(long, default_value_t = 80)]
pub height: usize, width: usize,
#[arg(long, default_value_t = 2)] #[arg(long, default_value_t = 25)]
pub pixel: usize, height: usize,
#[arg(long, default_value_t = 2)]
pixel: usize,
#[arg(long, default_value_t = false)]
noresize: bool,
#[arg(long, default_value_t = false)]
matrix: bool,
#[arg(long, default_value_t = false)]
nofill: bool,
#[arg(long, default_value_t = String::from(""))]
output: String,
#[arg(long, default_value_t = String::from(""))]
lightmap: String,
},
Cam {
#[arg(long, default_value_t = false)]
invert: bool,
#[arg(long, default_value_t = false)]
colorful: bool,
#[arg(long, default_value_t = 80)]
width: usize,
#[arg(long, default_value_t = 25)]
height: usize,
#[arg(long, default_value_t = 2)]
pixel: usize,
#[arg(long, default_value_t = false)]
noresize: bool,
#[arg(long, default_value_t = false)]
matrix: bool,
#[arg(long, default_value_t = false)]
nofill: bool,
#[arg(long, default_value_t = String::from(""))]
lightmap: String,
},
Vid {
#[arg(short, long, default_value_t = String::from(""))]
input: String,
#[arg(long, default_value_t = false)]
invert: bool,
#[arg(long, default_value_t = false)]
colorful: bool,
#[arg(long, default_value_t = 80)]
width: usize,
#[arg(long, default_value_t = 25)]
height: usize,
#[arg(long, default_value_t = 2)]
pixel: usize,
#[arg(long, default_value_t = false)]
noresize: bool,
#[arg(long, default_value_t = false)]
matrix: bool,
#[arg(long, default_value_t = false)]
nofill: bool,
#[arg(long, default_value_t = String::from(""))]
lightmap: String,
},
} }

View File

@@ -1,99 +1,83 @@
use colored::Colorize;
use image; use image;
use image::GenericImageView; use image::GenericImageView;
use crate::libs::args;
use colored::Colorize;
fn grayscale_ascii(img: &image::DynamicImage, width: u32, height: u32, invert: bool, pixel_size: usize) -> String { fn calculate_brightness((red, green, blue): (u8, u8, u8), mapping: &str) -> u8 {
let mut ascii_img = String::new(); let pixel_iterator: Vec<u16> = vec![red.into(), green.into(), blue.into()];
let darkest_color: &u16 = pixel_iterator.iter().min().unwrap();
for y in 0..height { let brightest_color: &u16 = pixel_iterator.iter().max().unwrap();
for x in 0..width {
let pixel = img.get_pixel(x, y);
let red = pixel[0]; let mapping: u8 = match mapping {
let green = pixel[1]; "fullbright" => 255,
let blue = pixel[2]; "luminosity" => (0.21 * red as f32 + 0.72 * green as f32 + 0.07 * blue as f32) as u8,
"minmax" | "average" | _ => ((darkest_color + brightest_color) / 2) as u8,
};
let pixel_iterator: Vec<u16> = vec![red.into(), green.into(), blue.into()]; mapping
let darkest_color: &u16 = pixel_iterator.iter().min().unwrap();
let brightest_color: &u16 = pixel_iterator.iter().max().unwrap();
let mut brightness = ((darkest_color + brightest_color) / 2) as u8;
if invert {
brightness = 255 - brightness;
}
let char_pixel = select_char(&brightness).repeat(pixel_size);
ascii_img.push_str(&char_pixel);
}
ascii_img.push('\n');
}
ascii_img
}
fn colorful_ascii(img: &image::DynamicImage, width: u32, height: u32, pixel_size: usize) -> String {
let mut ascii_img = String::new();
for y in 0..height {
for x in 0..width {
let pixel = img.get_pixel(x, y);
let red = pixel[0];
let green = pixel[1];
let blue = pixel[2];
let pixel_iterator: Vec<u16> = vec![red.into(), green.into(), blue.into()];
let darkest_color: &u16 = pixel_iterator.iter().min().unwrap();
let brightest_color: &u16 = pixel_iterator.iter().max().unwrap();
let brightness = ((darkest_color + brightest_color) / 2) as u8;
let mut char_pixel = select_char(&brightness).repeat(pixel_size);
char_pixel = select_dominant_color((red, green, blue), char_pixel);
ascii_img.push_str(&char_pixel);
}
ascii_img.push('\n');
}
ascii_img
} }
fn select_char(brightness: &u8) -> String { fn select_char(brightness: &u8) -> String {
let char = match brightness { let char = match brightness {
0..=25 => " ", 0..=25 => " ",
26..=50 => ".", 26..=50 => ".",
51..=75 => ":", 51..=75 => ":",
76..=100 => "-", 76..=100 => "-",
101..=125 => "=", 101..=125 => "=",
126..=150 => "+", 126..=150 => "+",
151..=175 => "*", 151..=175 => "*",
176..=200 => "#", 176..=200 => "#",
201..=225 => "%", 201..=225 => "%",
226..=255 => "@", 226..=255 => "@",
}; };
char.into() char.into()
} }
fn select_dominant_color(pixel : (u8, u8, u8), char_pixel: String) -> String { fn select_dominant_color(pixel: (u8, u8, u8), char_pixel: String) -> String {
let (red, green, blue) = pixel; let (red, green, blue) = pixel;
let char_pixel = char_pixel.truecolor(red, green, blue).to_string(); char_pixel.truecolor(red, green, blue).to_string()
char_pixel
} }
pub fn to_ascii(img: &image::DynamicImage, args: &args::Args) -> String { pub fn to_ascii(
let (width, height) = img.dimensions(); img: &image::DynamicImage,
lightmap: &str,
pixel_format: usize,
invert: bool,
colorful: bool,
matrix: bool,
) -> String {
let mut ascii_img = String::new();
let (width, height) = img.dimensions();
if args.colorful { for y in 0..height {
return colorful_ascii(img, width, height, args.pixel); for x in 0..width {
} let pixel = img.get_pixel(x, y);
let (mut red, mut green, mut blue) = (pixel[0], pixel[1], pixel[2]);
grayscale_ascii(img, width, height, args.invert, args.pixel) let mut brightness = calculate_brightness((red, green, blue), lightmap);
}
if invert {
red = 255 - red;
green = 255 - green;
blue = 255 - blue;
brightness = 255 - brightness;
}
let mut char_pixel = select_char(&brightness).repeat(pixel_format);
if colorful {
char_pixel = select_dominant_color((red, green, blue), char_pixel);
}
if matrix {
char_pixel = char_pixel.bright_green().to_string();
}
ascii_img.push_str(&char_pixel);
}
ascii_img.push('\n');
}
ascii_img
}

53
src/libs/cam_handler.rs Normal file
View File

@@ -0,0 +1,53 @@
use crate::libs;
use image::DynamicImage;
use nokhwa::{
pixel_format::RgbFormat,
utils::{CameraIndex, RequestedFormat, RequestedFormatType},
Camera,
};
pub fn camera(
invert: bool,
colorful: bool,
width: usize,
height: usize,
pixel: usize,
noresize: bool,
matrix: bool,
nofill: bool,
lightmap: String,
) {
let index = CameraIndex::Index(0);
let requested =
RequestedFormat::new::<RgbFormat>(RequestedFormatType::AbsoluteHighestFrameRate);
let mut camera = Camera::new(index, requested).unwrap();
camera.open_stream().unwrap();
loop {
let frame = camera.frame().unwrap();
let decoded = frame.decode_image::<RgbFormat>().unwrap();
let mut img: DynamicImage = decoded.into();
if !noresize && !nofill {
img = libs::image::resize_image(&img, width, height);
eprintln!("Image resized");
libs::image::print_size(&img);
}
if !noresize && nofill {
img = libs::image::resize_image_no_fill(&img, width, height);
eprintln!("Image resized exact");
libs::image::print_size(&img);
}
let ascii_img = libs::ascii::to_ascii(&img, &lightmap, pixel, invert, colorful, matrix);
eprintln!("ASCII image created");
clearscreen::clear().expect("Failed to clear screen");
print!("{ascii_img}");
}
}

53
src/libs/gen_handler.rs Normal file
View File

@@ -0,0 +1,53 @@
use crate::libs;
pub fn generator(
image_path: String,
invert: bool,
colorful: bool,
width: usize,
height: usize,
pixel: usize,
noresize: bool,
matrix: bool,
nofill: bool,
output: String,
lightmap: String,
) {
let path = image_path.clone();
let mut img = if !path.is_empty() {
libs::image::load_image(&path)
} else {
match libs::image::load_image_from_stdin() {
Ok(img) => img,
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
};
libs::image::print_size(&img);
if !noresize && !nofill {
img = libs::image::resize_image(&img, width, height);
eprintln!("Image resized");
libs::image::print_size(&img);
}
if !noresize && nofill {
img = libs::image::resize_image_no_fill(&img, width, height);
eprintln!("Image resized exact");
libs::image::print_size(&img);
}
let ascii_img = libs::ascii::to_ascii(&img, &lightmap, pixel, invert, colorful, matrix);
eprintln!("ASCII image created");
if !output.is_empty() {
libs::image::save_image(&ascii_img, &output);
return;
}
print!("{ascii_img}");
}

View File

@@ -1,18 +1,51 @@
use image::io::Reader;
use image; use image;
use image::GenericImageView; use image::GenericImageView;
use std::io::{self, BufReader, BufRead, Cursor};
use atty::Stream;
use std::fs;
pub fn load_image_from_stdin() -> Result<image::DynamicImage, image::ImageError> {
let mut buffer: Vec<u8> = Vec::new();
let mut raw_reader: Box<dyn BufRead> = if atty::is(Stream::Stdin) {
eprintln!("Error: No image provided");
std::process::exit(1);
} else {
Box::new(BufReader::new(io::stdin()))
};
raw_reader.read_to_end(&mut buffer).unwrap();
let reader = Reader::new(Cursor::new(buffer))
.with_guessed_format()
.expect("Failed to read image format");
eprintln!("Image loaded: stdin");
reader.decode()
}
pub fn load_image(file_name: &str) -> image::DynamicImage { pub fn load_image(file_name: &str) -> image::DynamicImage {
let img = image::open(file_name).expect("File not found!"); let img = image::open(file_name).expect("File not found!");
println!("Image loaded: {file_name}"); eprintln!("Image loaded: {file_name}");
img img
} }
pub fn save_image(img: &String, file_name: &str) {
fs::write(file_name, img).expect("Unable to write file");
eprintln!("Image saved: {file_name}");
}
pub fn print_size(img: &image::DynamicImage) { pub fn print_size(img: &image::DynamicImage) {
let (width, height) = img.dimensions(); let (width, height) = img.dimensions();
println!("Image dimensions: {width}x{height}"); eprintln!("Image dimensions: {width}x{height}");
} }
pub fn resize_image(img: &image::DynamicImage, nwidth: usize, nheight: usize) -> image::DynamicImage { pub fn resize_image(img: &image::DynamicImage, nwidth: usize, nheight: usize) -> image::DynamicImage {
img.resize(nwidth as u32, nheight as u32, image::imageops::FilterType::Lanczos3) img.resize(nwidth as u32, nheight as u32, image::imageops::FilterType::Lanczos3)
}
pub fn resize_image_no_fill(img: &image::DynamicImage, nwidth: usize, nheight: usize) -> image::DynamicImage {
img.resize_exact(nwidth as u32, nheight as u32, image::imageops::FilterType::Lanczos3)
} }

116
src/libs/vid_handler.rs Normal file
View File

@@ -0,0 +1,116 @@
use std::time::Duration;
use crate::libs;
use image::{DynamicImage, ImageBuffer};
use ffmpeg_next::format::{input, Pixel};
use ffmpeg_next::media::Type;
use ffmpeg_next::software::scaling::{context::Context, flag::Flags};
use ffmpeg_next::util::frame::video::Video;
pub fn video(
input_path: String,
invert: bool,
colorful: bool,
width: usize,
height: usize,
pixel: usize,
noresize: bool,
matrix: bool,
nofill: bool,
lightmap: String,
) {
ffmpeg_next::init().unwrap();
if input_path.is_empty() {
eprintln!("Error: No input path provided");
std::process::exit(1);
}
if let Ok(mut ictx) = input(&input_path) {
let input = ictx
.streams()
.best(Type::Video)
.ok_or(ffmpeg_next::Error::StreamNotFound)
.expect("Failed to find video stream");
let video_stream_index = input.index();
let context_decoder =
ffmpeg_next::codec::context::Context::from_parameters(input.parameters()).unwrap();
let mut decoder = context_decoder.decoder().video().unwrap();
let mut scaler = Context::get(
decoder.format(),
decoder.width(),
decoder.height(),
Pixel::RGB24,
decoder.width(),
decoder.height(),
Flags::BILINEAR,
)
.unwrap();
let frame_rate = input.avg_frame_rate();
let frame_duration = Duration::from_secs_f64(frame_rate.denominator() as f64 / frame_rate.numerator() as f64);
let mut frame_index = 0;
let mut receive_and_process_decoded_frames =
|decoder: &mut ffmpeg_next::decoder::Video| -> Result<(), ffmpeg_next::Error> {
let mut decoded = Video::empty();
while decoder.receive_frame(&mut decoded).is_ok() {
let start = std::time::Instant::now();
let mut rgb_frame = Video::empty();
scaler.run(&decoded, &mut rgb_frame)?;
let vwidth = rgb_frame.width();
let vheight = rgb_frame.height();
let data = rgb_frame.data(0);
let buf = ImageBuffer::from_raw(vwidth, vheight, data.to_vec()).unwrap();
let mut img = DynamicImage::ImageRgb8(buf);
if !noresize && !nofill {
img = libs::image::resize_image(&img, width, height);
eprintln!("Image resized");
libs::image::print_size(&img);
}
if !noresize && nofill {
img = libs::image::resize_image_no_fill(&img, width, height);
eprintln!("Image resized exact");
libs::image::print_size(&img);
}
let ascii_img =
libs::ascii::to_ascii(&img, &lightmap, pixel, invert, colorful, matrix);
eprintln!("ASCII image created");
clearscreen::clear().expect("Failed to clear screen");
print!("{ascii_img}");
frame_index += 1;
let elapsed = start.elapsed();
if elapsed < frame_duration {
std::thread::sleep(frame_duration - elapsed);
}
}
Ok(())
};
for (stream, packet) in ictx.packets() {
if stream.index() == video_stream_index {
decoder.send_packet(&packet).unwrap();
receive_and_process_decoded_frames(&mut decoder).unwrap();
}
}
decoder.send_eof().unwrap();
receive_and_process_decoded_frames(&mut decoder).unwrap();
}
}

View File

@@ -1,20 +1,65 @@
use clap::Parser; use clap::Parser;
mod libs; mod libs;
fn main() { fn main() {
println!("ASCII Generator\n----------------"); eprintln!("ASCII Generator\n----------------");
let args = libs::args::Args::parse(); let args = libs::args::Args::parse();
let img = libs::image::load_image(&args.image); match args.command {
libs::image::print_size(&img); Some(libs::args::Subcommands::Gen {
image,
let resized_img = libs::image::resize_image(&img, args.width, args.height); invert,
println!("Image resized"); colorful,
libs::image::print_size(&resized_img); width,
height,
let ascii_img = libs::ascii::to_ascii(&resized_img, &args); pixel,
println!("ASCII image created"); noresize,
matrix,
println!("{ascii_img}"); nofill,
} output,
lightmap,
}) => {
libs::gen_handler::generator(
image, invert, colorful, width, height, pixel, noresize, matrix, nofill, output,
lightmap,
);
}
Some(libs::args::Subcommands::Cam {
invert,
colorful,
width,
height,
pixel,
noresize,
matrix,
nofill,
lightmap,
}) => {
libs::cam_handler::camera(
invert, colorful, width, height, pixel, noresize, matrix, nofill, lightmap,
);
}
Some(libs::args::Subcommands::Vid {
input,
invert,
colorful,
width,
height,
pixel,
noresize,
matrix,
nofill,
lightmap,
}) => {
libs::vid_handler::video(
input, invert, colorful, width, height, pixel, noresize, matrix, nofill, lightmap,
);
}
None => {
eprintln!("No subcommand provided. Available subcommands: `gen`, `cam`, and `vid`");
std::process::exit(1);
}
}
}