implement histogram correction from CCCB_Ledwand
This commit is contained in:
parent
24fcd7a6fe
commit
c3aaf609ef
|
@ -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…
Reference in a new issue