From c3aaf609ef590b75be1146421dba4358a97eea4a Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Fri, 28 Feb 2025 10:48:03 +0100 Subject: [PATCH] implement histogram correction from CCCB_Ledwand --- src/brightness.rs | 6 +- src/cli.rs | 7 ++- src/ledwand_dither.rs | 127 ++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + src/pixels.rs | 6 +- src/stream_stdin.rs | 1 + src/stream_window.rs | 27 +++++---- 7 files changed, 158 insertions(+), 17 deletions(-) create mode 100644 src/ledwand_dither.rs diff --git a/src/brightness.rs b/src/brightness.rs index 09b7e55..adead44 100644 --- a/src/brightness.rs +++ b/src/brightness.rs @@ -1,6 +1,6 @@ -use servicepoint::{Brightness, Command, Connection}; -use log::info; use crate::cli::BrightnessCommand; +use log::info; +use servicepoint::{Brightness, Command, Connection}; pub(crate) fn brightness(connection: &Connection, brightness_command: BrightnessCommand) { match brightness_command { @@ -17,4 +17,4 @@ pub(crate) fn brightness_set(connection: &Connection, brightness: Brightness) { .send(Command::Brightness(brightness)) .expect("Failed to set brightness"); info!("set brightness to {brightness:?}"); -} \ No newline at end of file +} diff --git a/src/cli.rs b/src/cli.rs index 8f205f1..12a478e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -114,7 +114,12 @@ pub enum StreamCommand { #[derive(clap::Parser, std::fmt::Debug, Clone)] pub struct StreamScreenOptions { - #[arg(long, short, default_value_t = false, help = "Disable dithering - improves performance")] + #[arg( + long, + short, + default_value_t = false, + help = "Disable dithering - improves performance" + )] pub no_dither: bool, #[arg( diff --git a/src/ledwand_dither.rs b/src/ledwand_dither.rs new file mode 100644 index 0000000..c235a95 --- /dev/null +++ b/src/ledwand_dither.rs @@ -0,0 +1,127 @@ +//! Based on https://github.com/WarkerAnhaltRanger/CCCB_Ledwand + +use image::GrayImage; +use servicepoint::{PIXEL_HEIGHT, PIXEL_WIDTH}; + +pub struct LedwandDither { + options: LedwandDitherOptions, + tmpbuf: GrayImage, +} + +#[derive(Debug, Default)] +pub struct LedwandDitherOptions { + pub size: Option<(u32, u32)>, +} + +type GrayHistogram = [usize; 256]; + +struct HistogramCorrection { + pre_offset: f32, + post_offset: f32, + factor: f32, +} + +impl LedwandDither { + pub fn new(options: LedwandDitherOptions) -> Self { + let (width, height) = options + .size + .unwrap_or((PIXEL_WIDTH as u32, PIXEL_HEIGHT as u32)); + Self { + tmpbuf: GrayImage::new(width, height), + options, + } + } + + pub fn histogram_correction(image: &mut GrayImage) { + let histogram = Self::make_histogram(image); + let correction = Self::determine_histogram_correction(image, histogram); + Self::apply_histogram_correction(image, correction) + } + + fn make_histogram(image: &GrayImage) -> GrayHistogram { + let mut histogram = [0; 256]; + for pixel in image.pixels() { + histogram[pixel.0[0] as usize] += 1; + } + histogram + } + + fn determine_histogram_correction( + image: &GrayImage, + histogram: GrayHistogram, + ) -> HistogramCorrection { + let adjustment_pixels = image.len() / 100; + + let mut num_pixels = 0; + let mut brightness = 0; + + let mincut = loop { + num_pixels += histogram[brightness as usize] as usize; + brightness += 1; + if num_pixels >= adjustment_pixels { + break u8::min(brightness, 20); + } + }; + + let minshift = loop { + num_pixels += histogram[brightness as usize] as usize; + brightness += 1; + if num_pixels >= 2 * adjustment_pixels { + break u8::min(brightness, 64); + } + }; + + brightness = u8::MAX; + num_pixels = 0; + let maxshift = loop { + num_pixels += histogram[brightness as usize] as usize; + brightness -= 1; + if num_pixels >= 2 * adjustment_pixels { + break u8::max(brightness, 192); + } + }; + + let pre_offset = -(mincut as f32 / 2.); + let post_offset = -(minshift as f32); + let factor = (255.0 - post_offset) / maxshift as f32; + HistogramCorrection { + pre_offset, + post_offset, + factor, + } + } + + fn apply_histogram_correction(image: &mut GrayImage, correction: HistogramCorrection) { + let midpoint = image.width() / 2; + for (x, _, pixel) in image.enumerate_pixels_mut() { + if x > midpoint { + continue; + } + + let pixel = &mut pixel.0[0]; + let value = (*pixel as f32 + correction.pre_offset) * correction.factor + + correction.post_offset; + *pixel = value.clamp(0f32, u8::MAX as f32) as u8; + } + } + + pub fn median_brightness(image: &GrayImage) -> u8 { + let histogram = Self::make_histogram(image); + let midpoint = image.len() / 2; + + debug_assert_eq!( + image.len(), + histogram.iter().copied().map(usize::from).sum() + ); + + let mut num_pixels = 0; + for brightness in u8::MIN..=u8::MAX { + num_pixels += histogram[brightness as usize] as usize; + if num_pixels >= midpoint { + return brightness; + } + } + + unreachable!("Somehow less pixels where counted in the histogram than exist in the image") + } +} diff --git a/src/main.rs b/src/main.rs index bbdd20a..cd7937c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ use servicepoint::{Brightness, Connection}; mod brightness; mod cli; +mod ledwand_dither; mod pixels; mod stream_stdin; mod stream_window; diff --git a/src/pixels.rs b/src/pixels.rs index 0e4597e..4e6e3cf 100644 --- a/src/pixels.rs +++ b/src/pixels.rs @@ -1,6 +1,6 @@ -use servicepoint::{BitVec, Command, CompressionCode, Connection, PIXEL_COUNT}; -use log::info; use crate::cli::PixelCommand; +use log::info; +use servicepoint::{BitVec, Command, CompressionCode, Connection, PIXEL_COUNT}; pub(crate) fn pixels(connection: &Connection, pixel_command: PixelCommand) { match pixel_command { @@ -31,4 +31,4 @@ pub(crate) fn pixels_off(connection: &Connection) { .send(Command::Clear) .expect("failed to clear pixels"); info!("reset pixels"); -} \ No newline at end of file +} diff --git a/src/stream_stdin.rs b/src/stream_stdin.rs index a349aa2..82109ba 100644 --- a/src/stream_stdin.rs +++ b/src/stream_stdin.rs @@ -72,6 +72,7 @@ impl App { fn single_line(&mut self, line: &str) { let mut line_grid = CharGrid::new(TILE_WIDTH, 1); + line_grid.fill(' '); Self::line_onto_grid(&mut line_grid, 0, line); Self::line_onto_grid(&mut self.mirror, self.y, line); self.connection diff --git a/src/stream_window.rs b/src/stream_window.rs index e3a626a..db2b0bd 100644 --- a/src/stream_window.rs +++ b/src/stream_window.rs @@ -1,4 +1,5 @@ use crate::cli::StreamScreenOptions; +use crate::ledwand_dither::{LedwandDither, LedwandDitherOptions}; use image::{ imageops::{dither, resize, BiLevel, FilterType}, DynamicImage, ImageBuffer, Luma, Rgb, Rgba, @@ -26,9 +27,18 @@ pub fn stream_window(connection: &Connection, options: StreamScreenOptions) { let mut bitmap = Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT); info!("now starting to stream images"); loop { - let frame = get_next_frame(&capturer, options.no_dither); + let mut frame = get_next_frame(&capturer); + + LedwandDither::histogram_correction(&mut frame); + let cutoff = if options.no_dither { + LedwandDither::median_brightness(&frame) + } else { + dither(&mut frame, &BiLevel); + u8::MAX / 2 + }; + for (mut dest, src) in bitmap.iter_mut().zip(frame.pixels()) { - *dest = src.0[0] > u8::MAX / 2; + *dest = src.0[0] > cutoff; } connection @@ -41,21 +51,18 @@ pub fn stream_window(connection: &Connection, options: StreamScreenOptions) { } } -fn get_next_frame(capturer: &Capturer, no_dither: bool) -> ImageBuffer, Vec> { +/// returns next frame from the capturer, resized and grayscale +fn get_next_frame(capturer: &Capturer) -> ImageBuffer, Vec> { let frame = capturer.get_next_frame().expect("failed to capture frame"); let frame = frame_to_image(frame); let frame = frame.grayscale().to_luma8(); - let mut frame = resize( + + resize( &frame, PIXEL_WIDTH as u32, PIXEL_HEIGHT as u32, FilterType::Nearest, - ); - - if !no_dither { - dither(&mut frame, &BiLevel); - } - frame + ) } fn start_capture(options: &StreamScreenOptions) -> Option {