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 crate::cli::BrightnessCommand;
|
||||||
|
use log::info;
|
||||||
|
use servicepoint::{Brightness, Command, Connection};
|
||||||
|
|
||||||
pub(crate) fn brightness(connection: &Connection, brightness_command: BrightnessCommand) {
|
pub(crate) fn brightness(connection: &Connection, brightness_command: BrightnessCommand) {
|
||||||
match brightness_command {
|
match brightness_command {
|
||||||
|
@ -17,4 +17,4 @@ pub(crate) fn brightness_set(connection: &Connection, brightness: Brightness) {
|
||||||
.send(Command::Brightness(brightness))
|
.send(Command::Brightness(brightness))
|
||||||
.expect("Failed to set brightness");
|
.expect("Failed to set brightness");
|
||||||
info!("set brightness to {brightness:?}");
|
info!("set brightness to {brightness:?}");
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,7 +114,12 @@ pub enum StreamCommand {
|
||||||
|
|
||||||
#[derive(clap::Parser, std::fmt::Debug, Clone)]
|
#[derive(clap::Parser, std::fmt::Debug, Clone)]
|
||||||
pub struct StreamScreenOptions {
|
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,
|
pub no_dither: bool,
|
||||||
|
|
||||||
#[arg(
|
#[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 brightness;
|
||||||
mod cli;
|
mod cli;
|
||||||
|
mod ledwand_dither;
|
||||||
mod pixels;
|
mod pixels;
|
||||||
mod stream_stdin;
|
mod stream_stdin;
|
||||||
mod stream_window;
|
mod stream_window;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use servicepoint::{BitVec, Command, CompressionCode, Connection, PIXEL_COUNT};
|
|
||||||
use log::info;
|
|
||||||
use crate::cli::PixelCommand;
|
use crate::cli::PixelCommand;
|
||||||
|
use log::info;
|
||||||
|
use servicepoint::{BitVec, Command, CompressionCode, Connection, PIXEL_COUNT};
|
||||||
|
|
||||||
pub(crate) fn pixels(connection: &Connection, pixel_command: PixelCommand) {
|
pub(crate) fn pixels(connection: &Connection, pixel_command: PixelCommand) {
|
||||||
match pixel_command {
|
match pixel_command {
|
||||||
|
@ -31,4 +31,4 @@ pub(crate) fn pixels_off(connection: &Connection) {
|
||||||
.send(Command::Clear)
|
.send(Command::Clear)
|
||||||
.expect("failed to clear pixels");
|
.expect("failed to clear pixels");
|
||||||
info!("reset pixels");
|
info!("reset pixels");
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,6 +72,7 @@ impl App {
|
||||||
|
|
||||||
fn single_line(&mut self, line: &str) {
|
fn single_line(&mut self, line: &str) {
|
||||||
let mut line_grid = CharGrid::new(TILE_WIDTH, 1);
|
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 line_grid, 0, line);
|
||||||
Self::line_onto_grid(&mut self.mirror, self.y, line);
|
Self::line_onto_grid(&mut self.mirror, self.y, line);
|
||||||
self.connection
|
self.connection
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::cli::StreamScreenOptions;
|
use crate::cli::StreamScreenOptions;
|
||||||
|
use crate::ledwand_dither::{LedwandDither, LedwandDitherOptions};
|
||||||
use image::{
|
use image::{
|
||||||
imageops::{dither, resize, BiLevel, FilterType},
|
imageops::{dither, resize, BiLevel, FilterType},
|
||||||
DynamicImage, ImageBuffer, Luma, Rgb, Rgba,
|
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);
|
let mut bitmap = Bitmap::new(PIXEL_WIDTH, PIXEL_HEIGHT);
|
||||||
info!("now starting to stream images");
|
info!("now starting to stream images");
|
||||||
loop {
|
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()) {
|
for (mut dest, src) in bitmap.iter_mut().zip(frame.pixels()) {
|
||||||
*dest = src.0[0] > u8::MAX / 2;
|
*dest = src.0[0] > cutoff;
|
||||||
}
|
}
|
||||||
|
|
||||||
connection
|
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 = capturer.get_next_frame().expect("failed to capture frame");
|
||||||
let frame = frame_to_image(frame);
|
let frame = frame_to_image(frame);
|
||||||
let frame = frame.grayscale().to_luma8();
|
let frame = frame.grayscale().to_luma8();
|
||||||
let mut frame = resize(
|
|
||||||
|
resize(
|
||||||
&frame,
|
&frame,
|
||||||
PIXEL_WIDTH as u32,
|
PIXEL_WIDTH as u32,
|
||||||
PIXEL_HEIGHT as u32,
|
PIXEL_HEIGHT as u32,
|
||||||
FilterType::Nearest,
|
FilterType::Nearest,
|
||||||
);
|
)
|
||||||
|
|
||||||
if !no_dither {
|
|
||||||
dither(&mut frame, &BiLevel);
|
|
||||||
}
|
|
||||||
frame
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_capture(options: &StreamScreenOptions) -> Option<Capturer> {
|
fn start_capture(options: &StreamScreenOptions) -> Option<Capturer> {
|
||||||
|
|
Loading…
Reference in a new issue