use fovea::analyze::histogram::Histogram;
use fovea::image::{Image, ImageViewMut};
use fovea::pixel::Srgba8;
use crate::DisplayContext;
use crate::strategy::{Framebuffer, Identity};
#[derive(Debug, Clone, Copy)]
pub struct HistogramPlotOptions {
pub width: u32,
pub height: u32,
pub padding: u32,
pub background: Srgba8,
pub axis: Srgba8,
pub log_scale: bool,
}
impl Default for HistogramPlotOptions {
fn default() -> Self {
Self {
width: 512,
height: 256,
padding: 8,
background: Srgba8::new(24, 24, 28, 255),
axis: Srgba8::new(96, 96, 104, 255),
log_scale: false,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct HistogramLayer<'a> {
pub bins: &'a [u64],
pub color: Srgba8,
}
impl<'a> HistogramLayer<'a> {
#[inline]
pub fn new(bins: &'a [u64], color: Srgba8) -> Self {
Self { bins, color }
}
#[inline]
pub fn from_histogram<S, V>(h: &'a Histogram<S, V>, color: Srgba8) -> Self {
Self::new(h.bins(), color)
}
}
#[derive(Debug, Clone, Copy)]
pub struct HistogramRenderOptions {
pub width: u32,
pub height: u32,
pub padding: u32,
pub background: Srgba8,
pub bar: Srgba8,
pub axis: Srgba8,
pub log_scale: bool,
}
impl Default for HistogramRenderOptions {
fn default() -> Self {
Self {
width: 512,
height: 256,
padding: 8,
background: Srgba8::new(24, 24, 28, 255),
bar: Srgba8::new(180, 200, 230, 255),
axis: Srgba8::new(96, 96, 104, 255),
log_scale: false,
}
}
}
impl HistogramRenderOptions {
#[inline]
fn split(&self) -> (HistogramPlotOptions, Srgba8) {
(
HistogramPlotOptions {
width: self.width,
height: self.height,
padding: self.padding,
background: self.background,
axis: self.axis,
log_scale: self.log_scale,
},
self.bar,
)
}
}
pub fn render_histogram_layers(
layers: &[HistogramLayer<'_>],
opts: &HistogramPlotOptions,
) -> Image<Srgba8> {
let w = opts.width.max(1) as usize;
let height_px = opts.height.max(1) as usize;
let pad = (opts.padding as usize).min(w / 4).min(height_px / 4);
let mut img = Image::fill(w, height_px, opts.background);
let plot_x0 = pad;
let plot_y0 = pad;
let plot_x1 = w.saturating_sub(pad).max(plot_x0 + 1);
let plot_y1 = height_px.saturating_sub(pad).max(plot_y0 + 1);
let plot_w = plot_x1 - plot_x0;
let plot_h = plot_y1 - plot_y0;
let baseline_y = plot_y1.saturating_sub(1);
for x in plot_x0..plot_x1 {
*img.pixel_at_mut(x, baseline_y) = opts.axis;
}
if layers.is_empty() || plot_w == 0 || plot_h == 0 {
return img;
}
let transform = |c: u64| -> f64 {
if opts.log_scale {
(1.0 + c as f64).ln()
} else {
c as f64
}
};
let mut max_val = 0.0_f64;
for layer in layers {
for &c in layer.bins {
let v = transform(c);
if v > max_val {
max_val = v;
}
}
}
if max_val <= 0.0 {
return img;
}
let usable_h = plot_h.saturating_sub(1); if usable_h == 0 {
return img;
}
for layer in layers {
let n = layer.bins.len();
if n == 0 {
continue;
}
for col in 0..plot_w {
let lo = (col * n) / plot_w;
let hi_excl = (((col + 1) * n) / plot_w).max(lo + 1).min(n);
let mut local_max = 0.0_f64;
for b in &layer.bins[lo..hi_excl] {
let v = transform(*b);
if v > local_max {
local_max = v;
}
}
if local_max <= 0.0 {
continue;
}
let bar_h = ((local_max / max_val) * usable_h as f64).round() as usize;
let bar_h = bar_h.min(usable_h);
if bar_h == 0 {
continue;
}
let x = plot_x0 + col;
let top = baseline_y.saturating_sub(bar_h);
for y in top..baseline_y {
let dst = img.pixel_at_mut(x, y);
*dst = blend_over(layer.color, *dst);
}
}
}
img
}
pub fn render_histogram<S, V>(h: &Histogram<S, V>, opts: &HistogramRenderOptions) -> Image<Srgba8> {
let (plot, bar) = opts.split();
let layers = [HistogramLayer::new(h.bins(), bar)];
render_histogram_layers(&layers, &plot)
}
#[inline]
fn blend_over(src: Srgba8, dst: Srgba8) -> Srgba8 {
let sa = src.a.0 as u32;
if sa == 0 {
return dst;
}
if sa == 255 {
return src;
}
let inv = 255 - sa;
let mix = |s: u8, d: u8| -> u8 {
let v = (s as u32) * sa + (d as u32) * inv;
((v + 127) / 255) as u8
};
let da = dst.a.0 as u32;
let out_a = sa + (da * inv + 127) / 255;
let out_a = out_a.min(255) as u8;
Srgba8::new(
mix(src.r.0, dst.r.0),
mix(src.g.0, dst.g.0),
mix(src.b.0, dst.b.0),
out_a,
)
}
pub fn debug_histogram<S, V>(title: &str, h: &Histogram<S, V>) {
debug_histogram_with(title, h, &HistogramRenderOptions::default());
}
pub fn debug_histogram_with<S, V>(title: &str, h: &Histogram<S, V>, opts: &HistogramRenderOptions) {
let img = render_histogram(h, opts);
crate::show(title, &img, Identity);
}
pub fn debug_histogram_layers(
title: &str,
layers: &[HistogramLayer<'_>],
opts: &HistogramPlotOptions,
) {
let img = render_histogram_layers(layers, opts);
crate::show(title, &img, Identity);
}
impl DisplayContext {
pub fn show_histogram<S, V>(&self, title: &str, h: &Histogram<S, V>) {
self.show_histogram_with(title, h, &HistogramRenderOptions::default());
}
pub fn show_histogram_with<S, V>(
&self,
title: &str,
h: &Histogram<S, V>,
opts: &HistogramRenderOptions,
) {
let img = render_histogram(h, opts);
let fb = Framebuffer::from_image(&img, Identity);
self.show_framebuffer(title, fb);
}
pub fn show_histogram_layers(
&self,
title: &str,
layers: &[HistogramLayer<'_>],
opts: &HistogramPlotOptions,
) {
let img = render_histogram_layers(layers, opts);
let fb = Framebuffer::from_image(&img, Identity);
self.show_framebuffer(title, fb);
}
}
#[cfg(test)]
mod tests {
use super::*;
use fovea::analyze::histogram::{NaturalBins, histogram};
use fovea::image::ImageView;
use fovea::pixel::Mono8;
fn build_hist() -> Histogram<NaturalBins, std::num::Saturating<u8>> {
let mut img: Image<Mono8> = Image::fill(4, 4, Mono8::new(255));
*img.pixel_at_mut(0, 0) = Mono8::new(0);
histogram(&img, &NaturalBins).unwrap()
}
#[test]
fn render_default_size_matches_options() {
let h = build_hist();
let opts = HistogramRenderOptions::default();
let img = render_histogram(&h, &opts);
assert_eq!(img.width(), opts.width as usize);
assert_eq!(img.height(), opts.height as usize);
}
#[test]
fn render_zero_size_padded_does_not_panic() {
let h = build_hist();
let opts = HistogramRenderOptions {
width: 1,
height: 1,
padding: 0,
..HistogramRenderOptions::default()
};
let img = render_histogram(&h, &opts);
assert_eq!(img.width(), 1);
assert_eq!(img.height(), 1);
}
#[test]
fn render_paints_background_outside_bars() {
let h = build_hist();
let opts = HistogramRenderOptions::default();
let img = render_histogram(&h, &opts);
assert_eq!(img.pixel_at(0, 0), opts.background);
}
#[test]
fn render_log_scale_runs() {
let h = build_hist();
let opts = HistogramRenderOptions {
log_scale: true,
..HistogramRenderOptions::default()
};
let _ = render_histogram(&h, &opts);
}
#[test]
fn render_empty_bins_is_safe() {
let h = build_hist();
let opts = HistogramRenderOptions {
width: 4,
height: 4,
padding: 2,
..HistogramRenderOptions::default()
};
let img = render_histogram(&h, &opts);
assert_eq!(img.width(), 4);
assert_eq!(img.height(), 4);
}
#[test]
fn layered_no_layers_yields_background_and_axis_only() {
let opts = HistogramPlotOptions::default();
let img = render_histogram_layers(&[], &opts);
assert_eq!(img.width(), opts.width as usize);
assert_eq!(img.height(), opts.height as usize);
assert_eq!(img.pixel_at(0, 0), opts.background);
}
#[test]
fn layered_two_translucent_layers_blend() {
let h = build_hist();
let opts = HistogramPlotOptions::default();
let layers = [
HistogramLayer::from_histogram(&h, Srgba8::new(255, 0, 0, 128)),
HistogramLayer::from_histogram(&h, Srgba8::new(0, 0, 255, 128)),
];
let img = render_histogram_layers(&layers, &opts);
assert_eq!(img.width(), opts.width as usize);
assert_eq!(img.height(), opts.height as usize);
}
#[test]
fn blend_over_zero_alpha_is_passthrough() {
let dst = Srgba8::new(10, 20, 30, 200);
let src = Srgba8::new(255, 0, 0, 0);
assert_eq!(blend_over(src, dst), dst);
}
#[test]
fn blend_over_full_alpha_replaces_destination() {
let dst = Srgba8::new(10, 20, 30, 200);
let src = Srgba8::new(255, 0, 0, 255);
assert_eq!(blend_over(src, dst), src);
}
#[test]
fn blend_over_half_alpha_mixes_components() {
let dst = Srgba8::new(0, 0, 0, 255);
let src = Srgba8::new(255, 0, 0, 128);
let out = blend_over(src, dst);
assert!(out.r.0 >= 127 && out.r.0 <= 129, "r = {}", out.r.0);
assert_eq!(out.g.0, 0);
assert_eq!(out.b.0, 0);
assert_eq!(out.a.0, 255);
}
}