diff --git a/Cargo.lock b/Cargo.lock index 6097160..547fa91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1629,9 +1629,9 @@ dependencies = [ [[package]] name = "servicepoint" -version = "0.13.0" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a8bc9e40503ece07e3f12232f648484191323b8126e74abce3d1644ba04dbd0" +checksum = "33abd53582a995aaf5d387be4a1f7eb294a084185f88f8cf61652b6272041660" dependencies = [ "bitvec", "log", diff --git a/Cargo.toml b/Cargo.toml index 25daf24..0ae04d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ homepage = "https://crates.io/crates/servicepoint-cli" keywords = ["cccb", "cccb-servicepoint", "cli"] [dependencies] -servicepoint = { version = "0.13.0", features = ["protocol_websocket"] } +servicepoint = { version = "0.13.2", features = ["protocol_websocket"] } clap = { version = "4.5", features = ["derive"] } env_logger = "0.11" log = "0.4" diff --git a/src/cli.rs b/src/cli.rs index 12a478e..1ee5783 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -114,14 +114,6 @@ 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" - )] - pub no_dither: bool, - #[arg( long, short, diff --git a/src/ledwand_dither.rs b/src/ledwand_dither.rs index 135515c..be1091e 100644 --- a/src/ledwand_dither.rs +++ b/src/ledwand_dither.rs @@ -1,17 +1,7 @@ //! Based on https://github.com/WarkerAnhaltRanger/CCCB_Ledwand -use image::{GenericImage, 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)>, -} +use image::GrayImage; +use servicepoint::{BitVec, Bitmap, PIXEL_HEIGHT}; type GrayHistogram = [usize; 256]; @@ -21,164 +11,495 @@ struct HistogramCorrection { 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 = make_histogram(image); + let correction = determine_histogram_correction(image, histogram); + 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() / PIXEL_HEIGHT; + + let mut num_pixels = 0; + let mut brightness = 0; + + let mincut = loop { + num_pixels += histogram[brightness as usize]; + brightness += 1; + if num_pixels >= adjustment_pixels { + break u8::min(brightness, 20); + } + }; + + let minshift = loop { + num_pixels += histogram[brightness 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]; + 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) { + for pixel in image.pixels_mut() { + 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 = 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]; + if num_pixels >= midpoint { + return brightness; } } - 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) + unreachable!("Somehow less pixels where counted in the histogram than exist in the image") +} + +pub fn blur(source: &GrayImage, destination: &mut GrayImage) { + assert_eq!(source.len(), destination.len()); + + copy_border(source, destination); + blur_inner_pixels(source, destination); +} + +pub fn sharpen(source: &GrayImage, destination: &mut GrayImage) { + assert_eq!(source.len(), destination.len()); + + copy_border(source, destination); + sharpen_inner_pixels(source, destination); +} + +fn copy_border(source: &GrayImage, destination: &mut GrayImage) { + let last_row = source.height() - 1; + for x in 0..source.width() { + destination[(x, 0)] = source[(x, 0)]; + destination[(x, last_row)] = source[(x, last_row)]; } - - fn make_histogram(image: &GrayImage) -> GrayHistogram { - let mut histogram = [0; 256]; - for pixel in image.pixels() { - histogram[pixel.0[0] as usize] += 1; - } - histogram + let last_col = source.width() - 1; + for y in 0..source.height() { + destination[(0, y)] = source[(0, y)]; + destination[(last_col, y)] = source[(last_col, y)]; } +} - fn determine_histogram_correction( - image: &GrayImage, - histogram: GrayHistogram, - ) -> HistogramCorrection { - let adjustment_pixels = image.len() / PIXEL_HEIGHT; - - let mut num_pixels = 0; - let mut brightness = 0; - - let mincut = loop { - num_pixels += histogram[brightness as usize]; - brightness += 1; - if num_pixels >= adjustment_pixels { - break u8::min(brightness, 20); - } - }; - - let minshift = loop { - num_pixels += histogram[brightness 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]; - 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) { - for pixel in image.pixels_mut() { - 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]; - if num_pixels >= midpoint { - return brightness; - } - } - - unreachable!("Somehow less pixels where counted in the histogram than exist in the image") - } - - pub fn blur(source: &GrayImage, destination: &mut GrayImage) { - assert_eq!(source.len(), destination.len()); - - Self::copy_border(source, destination); - Self::blur_inner_pixels(source, destination); - } - - pub fn sharpen(source: &GrayImage, destination: &mut GrayImage) { - assert_eq!(source.len(), destination.len()); - - Self::copy_border(source, destination); - Self::sharpen_inner_pixels(source, destination); - } - - fn copy_border(source: &GrayImage, destination: &mut GrayImage) { - let last_row = source.height() -1; - for x in 0..source.width() { - destination[(x, 0)] = source[(x, 0)]; - destination[(x, last_row)] = source[(x, last_row)]; - } - let last_col = source.width() - 1; - for y in 0..source.height() { - destination[(0, y)] = source[(0, y)]; - destination[(last_col, y)] = source[(last_col, y)]; - } - } - - fn blur_inner_pixels(source: &GrayImage, destination: &mut GrayImage) { - for y in 1..source.height() - 2 { - for x in 1..source.width() - 2 { - let weighted_sum = source.get_pixel(x - 1, y - 1).0[0] as u32 - + source.get_pixel(x, y - 1).0[0] as u32 - + source.get_pixel(x + 1, y - 1).0[0] as u32 - + source.get_pixel(x - 1, y).0[0] as u32 - + 8 * source.get_pixel(x, y).0[0] as u32 - + source.get_pixel(x + 1, y).0[0] as u32 - + source.get_pixel(x - 1, y + 1).0[0] as u32 - + source.get_pixel(x, y + 1).0[0] as u32 - + source.get_pixel(x + 1, y + 1).0[0] as u32; - let blurred = weighted_sum / 16; - destination.get_pixel_mut(x, y).0[0] = blurred.clamp(u8::MIN as u32, u8::MAX as u32) as u8; - } - } - } - - fn sharpen_inner_pixels(source: &GrayImage, destination: &mut GrayImage) { - for y in 1..source.height() - 2 { - for x in 1..source.width() - 2 { - let weighted_sum = -(source.get_pixel(x - 1, y - 1).0[0] as i32) - - source.get_pixel(x, y - 1).0[0] as i32 - - source.get_pixel(x + 1, y - 1).0[0] as i32 - - source.get_pixel(x - 1, y).0[0] as i32 - + 9 * source.get_pixel(x, y).0[0] as i32 - - source.get_pixel(x + 1, y).0[0] as i32 - - source.get_pixel(x - 1, y + 1).0[0] as i32 - - source.get_pixel(x, y + 1).0[0] as i32 - - source.get_pixel(x + 1, y + 1).0[0] as i32; - destination.get_pixel_mut(x, y).0[0] = weighted_sum.clamp(u8::MIN as i32, u8::MAX as i32) as u8; - } +fn blur_inner_pixels(source: &GrayImage, destination: &mut GrayImage) { + for y in 1..source.height() - 2 { + for x in 1..source.width() - 2 { + let weighted_sum = source.get_pixel(x - 1, y - 1).0[0] as u32 + + source.get_pixel(x, y - 1).0[0] as u32 + + source.get_pixel(x + 1, y - 1).0[0] as u32 + + source.get_pixel(x - 1, y).0[0] as u32 + + 8 * source.get_pixel(x, y).0[0] as u32 + + source.get_pixel(x + 1, y).0[0] as u32 + + source.get_pixel(x - 1, y + 1).0[0] as u32 + + source.get_pixel(x, y + 1).0[0] as u32 + + source.get_pixel(x + 1, y + 1).0[0] as u32; + let blurred = weighted_sum / 16; + destination.get_pixel_mut(x, y).0[0] = + blurred.clamp(u8::MIN as u32, u8::MAX as u32) as u8; } } } + +fn sharpen_inner_pixels(source: &GrayImage, destination: &mut GrayImage) { + for y in 1..source.height() - 2 { + for x in 1..source.width() - 2 { + let weighted_sum = -(source.get_pixel(x - 1, y - 1).0[0] as i32) + - source.get_pixel(x, y - 1).0[0] as i32 + - source.get_pixel(x + 1, y - 1).0[0] as i32 + - source.get_pixel(x - 1, y).0[0] as i32 + + 9 * source.get_pixel(x, y).0[0] as i32 + - source.get_pixel(x + 1, y).0[0] as i32 + - source.get_pixel(x - 1, y + 1).0[0] as i32 + - source.get_pixel(x, y + 1).0[0] as i32 + - source.get_pixel(x + 1, y + 1).0[0] as i32; + destination.get_pixel_mut(x, y).0[0] = + weighted_sum.clamp(u8::MIN as i32, u8::MAX as i32) as u8; + } + } +} + +pub(crate) fn ostromoukhov_dither(source: GrayImage, bias: u8) -> Bitmap { + let width = source.width(); + let height = source.height(); + assert_eq!(width % 8, 0); + + let mut source = source.into_raw(); + let mut destination = BitVec::repeat(false, source.len()); + + for y in 0..height as usize { + let start = y * width as usize; + if y % 2 == 0 { + for x in 0..width as usize { + ostromoukhov_dither_pixel( + &mut source, + &mut destination, + start + x, + width as usize, + y == (height - 1) as usize, + 1, + bias, + ); + } + } else { + for x in (0..width as usize).rev() { + ostromoukhov_dither_pixel( + &mut source, + &mut destination, + start + x, + width as usize, + y == (height - 1) as usize, + -1, + bias, + ); + } + } + } + + Bitmap::from_bitvec(width as usize, destination) +} + +#[inline] +fn ostromoukhov_dither_pixel( + source: &mut [u8], + destination: &mut BitVec, + position: usize, + width: usize, + last_row: bool, + direction: isize, + bias: u8, +) { + let old_pixel = source[position]; + + let destination_value = old_pixel > bias; + destination.set(position, destination_value); + + let error = if destination_value { + 255 - old_pixel + } else { + old_pixel + }; + + let mut diffuse = |to: usize, mat: i16| { + let diffuse_value = source[to] as i16 + mat; + source[to] = diffuse_value.clamp(u8::MIN.into(), u8::MAX.into()) as u8; + }; + + let lookup = if destination_value { + ERROR_DIFFUSION_MATRIX[error as usize].map(move |i| -i) + } else { + ERROR_DIFFUSION_MATRIX[error as usize] + }; + diffuse((position as isize + direction) as usize, lookup[0]); + + if !last_row { + diffuse( + ((position + width) as isize - direction) as usize, + lookup[1], + ); + diffuse(((position + width) as isize) as usize, lookup[2]); + } +} + +const ERROR_DIFFUSION_MATRIX: [[i16; 3]; 256] = [ + [0, 1, 0], + [1, 0, 0], + [1, 0, 1], + [2, 0, 1], + [2, 0, 2], + [3, 0, 2], + [4, 0, 2], + [4, 1, 2], + [5, 1, 2], + [5, 2, 2], + [5, 3, 2], + [6, 3, 2], + [6, 3, 3], + [7, 3, 3], + [7, 4, 3], + [8, 4, 3], + [8, 5, 3], + [9, 5, 3], + [9, 5, 4], + [10, 6, 3], + [10, 6, 4], + [11, 7, 3], + [11, 7, 4], + [11, 8, 4], + [12, 7, 5], + [12, 7, 6], + [12, 7, 7], + [12, 7, 8], + [12, 7, 9], + [13, 7, 9], + [13, 7, 10], + [13, 7, 11], + [13, 7, 12], + [14, 7, 12], + [14, 8, 12], + [15, 8, 12], + [15, 9, 12], + [16, 9, 12], + [16, 10, 12], + [17, 10, 12], + [17, 11, 12], + [18, 12, 11], + [19, 12, 11], + [19, 13, 11], + [20, 13, 11], + [20, 14, 11], + [21, 15, 10], + [22, 15, 10], + [22, 17, 9], + [23, 17, 9], + [24, 18, 8], + [24, 19, 8], + [25, 19, 8], + [26, 20, 7], + [26, 21, 7], + [27, 22, 6], + [28, 23, 5], + [28, 24, 5], + [29, 25, 4], + [30, 26, 3], + [31, 26, 3], + [31, 28, 2], + [32, 28, 2], + [33, 29, 1], + [34, 30, 0], + [33, 31, 1], + [32, 33, 1], + [32, 33, 2], + [31, 34, 3], + [30, 36, 3], + [29, 37, 4], + [29, 37, 5], + [28, 39, 5], + [32, 34, 7], + [37, 29, 8], + [42, 23, 10], + [46, 19, 11], + [51, 13, 12], + [52, 14, 13], + [53, 13, 12], + [53, 14, 13], + [54, 14, 13], + [55, 14, 13], + [55, 14, 13], + [56, 15, 14], + [57, 14, 13], + [56, 15, 15], + [55, 17, 15], + [54, 18, 16], + [53, 20, 16], + [52, 21, 17], + [52, 22, 17], + [51, 24, 17], + [50, 25, 18], + [49, 27, 18], + [47, 29, 19], + [48, 29, 19], + [48, 29, 20], + [49, 29, 20], + [49, 30, 20], + [50, 31, 20], + [50, 31, 20], + [51, 31, 20], + [51, 31, 21], + [52, 31, 21], + [52, 32, 21], + [53, 32, 21], + [53, 32, 22], + [55, 32, 21], + [56, 31, 22], + [58, 31, 21], + [59, 30, 22], + [61, 30, 21], + [62, 29, 22], + [64, 29, 21], + [65, 28, 22], + [67, 28, 21], + [68, 27, 22], + [70, 27, 21], + [71, 26, 22], + [73, 26, 21], + [75, 25, 21], + [76, 25, 21], + [78, 24, 21], + [80, 23, 21], + [81, 23, 21], + [83, 22, 21], + [85, 21, 20], + [85, 22, 21], + [85, 22, 22], + [84, 24, 22], + [84, 24, 23], + [84, 25, 23], + [83, 27, 23], + [83, 28, 23], + [82, 29, 24], + [82, 30, 24], + [81, 31, 25], + [80, 32, 26], + [80, 33, 26], + [79, 35, 26], + [79, 36, 26], + [78, 37, 27], + [77, 38, 28], + [77, 39, 28], + [76, 41, 28], + [75, 42, 29], + [75, 43, 29], + [74, 44, 30], + [74, 45, 30], + [75, 46, 30], + [75, 46, 30], + [76, 46, 30], + [76, 46, 31], + [77, 46, 31], + [77, 47, 31], + [78, 47, 31], + [78, 47, 32], + [79, 47, 32], + [79, 48, 32], + [80, 49, 32], + [83, 46, 32], + [86, 44, 32], + [90, 42, 31], + [93, 40, 31], + [96, 39, 30], + [100, 36, 30], + [103, 35, 29], + [106, 33, 29], + [110, 30, 29], + [113, 29, 28], + [114, 29, 28], + [115, 29, 28], + [115, 29, 28], + [116, 30, 29], + [117, 29, 28], + [117, 30, 29], + [118, 30, 29], + [119, 30, 29], + [109, 43, 27], + [100, 57, 23], + [90, 71, 20], + [80, 85, 17], + [70, 99, 14], + [74, 98, 12], + [78, 97, 10], + [81, 96, 9], + [85, 95, 7], + [89, 94, 5], + [92, 93, 4], + [96, 92, 2], + [100, 91, 0], + [100, 90, 2], + [100, 88, 5], + [100, 87, 7], + [99, 86, 10], + [99, 85, 12], + [99, 84, 14], + [99, 82, 17], + [98, 81, 20], + [98, 80, 22], + [98, 79, 24], + [98, 77, 27], + [98, 76, 29], + [97, 75, 32], + [97, 73, 35], + [97, 72, 37], + [96, 71, 40], + [96, 69, 43], + [96, 67, 46], + [96, 66, 48], + [95, 65, 51], + [95, 63, 54], + [95, 61, 57], + [94, 60, 60], + [94, 58, 63], + [94, 57, 65], + [93, 55, 69], + [93, 54, 71], + [93, 52, 74], + [92, 51, 77], + [92, 49, 80], + [91, 47, 84], + [91, 46, 86], + [93, 49, 82], + [96, 52, 77], + [98, 55, 73], + [101, 58, 68], + [104, 61, 63], + [106, 65, 58], + [109, 68, 53], + [111, 71, 49], + [114, 74, 44], + [116, 78, 39], + [118, 76, 40], + [119, 74, 42], + [120, 73, 43], + [122, 71, 44], + [123, 69, 46], + [124, 67, 48], + [125, 66, 49], + [127, 64, 50], + [128, 62, 52], + [129, 60, 54], + [131, 58, 55], + [132, 57, 56], + [136, 47, 63], + [139, 38, 70], + [143, 29, 76], + [147, 19, 83], + [151, 9, 90], + [154, 0, 97], + [160, 0, 92], + [171, 0, 82], + [183, 0, 71], + [184, 0, 71], +]; diff --git a/src/stream_window.rs b/src/stream_window.rs index 0358c61..6975ebf 100644 --- a/src/stream_window.rs +++ b/src/stream_window.rs @@ -1,7 +1,7 @@ use crate::cli::StreamScreenOptions; -use crate::ledwand_dither::{LedwandDither, LedwandDitherOptions}; +use crate::ledwand_dither::*; use image::{ - imageops::{dither, resize, BiLevel, FilterType}, + imageops::{resize, FilterType}, DynamicImage, ImageBuffer, Luma, Rgb, Rgba, }; use log::{error, info, warn}; @@ -11,42 +11,31 @@ use scap::{ frame::Frame, }; use servicepoint::{ - Bitmap, Command, CompressionCode, Connection, Origin, FRAME_PACING, PIXEL_HEIGHT, PIXEL_WIDTH, + Command, CompressionCode, Connection, Origin, FRAME_PACING, PIXEL_HEIGHT, PIXEL_WIDTH, }; use std::time::Duration; pub fn stream_window(connection: &Connection, options: StreamScreenOptions) { info!("Starting capture with options: {:?}", options); - warn!("this implementation does not drop any frames - set a lower fps or disable dithering if your computer cannot keep up."); let capturer = match start_capture(&options) { Some(value) => value, None => return, }; - let mut bitmap = Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT); info!("now starting to stream images"); loop { let mut frame = get_next_frame(&capturer); - LedwandDither::histogram_correction(&mut frame); + histogram_correction(&mut frame); let mut orig = frame.clone(); - LedwandDither::blur(&orig, &mut frame); + blur(&orig, &mut frame); std::mem::swap(&mut frame, &mut orig); - LedwandDither::sharpen(&orig, &mut frame); + sharpen(&orig, &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] > cutoff; - } + let bitmap = ostromoukhov_dither(frame, u8::MAX / 2); connection .send(Command::BitmapLinearWin(