ostromoukhov dither
Some checks failed
Rust / build (pull_request) Failing after 2m3s

This commit is contained in:
Vinzenz Schroeter 2025-02-28 17:38:38 +01:00
parent 32f148bfef
commit f566f0959b
5 changed files with 495 additions and 193 deletions

4
Cargo.lock generated
View file

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

View file

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

View file

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

View file

@ -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],
];

View file

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