implement histogram correction from CCCB_Ledwand

This commit is contained in:
Vinzenz Schroeter 2025-02-28 10:48:03 +01:00
parent 24fcd7a6fe
commit c3aaf609ef
7 changed files with 158 additions and 17 deletions

View file

@ -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:?}");
}
}

View file

@ -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
View 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")
}
}

View file

@ -11,6 +11,7 @@ use servicepoint::{Brightness, Connection};
mod brightness;
mod cli;
mod ledwand_dither;
mod pixels;
mod stream_stdin;
mod stream_window;

View file

@ -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");
}
}

View file

@ -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

View file

@ -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> {