better dithering, keep aspect ratio, send image #2
					 7 changed files with 158 additions and 17 deletions
				
			
		|  | @ -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:?}"); | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -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(
 | ||||
|  |  | |||
							
								
								
									
										127
									
								
								src/ledwand_dither.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								src/ledwand_dither.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -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") | ||||
|     } | ||||
| } | ||||
|  | @ -11,6 +11,7 @@ use servicepoint::{Brightness, Connection}; | |||
| 
 | ||||
| mod brightness; | ||||
| mod cli; | ||||
| mod ledwand_dither; | ||||
| mod pixels; | ||||
| mod stream_stdin; | ||||
| mod stream_window; | ||||
|  |  | |||
|  | @ -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"); | ||||
| } | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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<Luma<u8>, Vec<u8>> { | ||||
| /// returns next frame from the capturer, resized and grayscale
 | ||||
| fn get_next_frame(capturer: &Capturer) -> ImageBuffer<Luma<u8>, Vec<u8>> { | ||||
|     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<Capturer> { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue