From 7dc132dad73234128d6f458287fd5af236802996 Mon Sep 17 00:00:00 2001 From: pingupingou Date: Fri, 7 Nov 2025 15:24:23 +0000 Subject: [PATCH] Added BxDF support, still somewhat broken. Starting work on image reading, writing. At the moment, this is completely broken. Fixed some issues with cameras, matrix operations, math --- README.md | 69 +++ src/utils/error.rs | 84 ++++ src/utils/image.rs | 1100 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1253 insertions(+) create mode 100644 README.md create mode 100644 src/utils/error.rs create mode 100644 src/utils/image.rs diff --git a/README.md b/README.md new file mode 100644 index 0000000..4d5df0d --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# PBRust + +## Description + +A Rust implementation of the physically based renderer described in the tremendous book *Physically Based Rendering: From Theory to Implementation* by Matt Pharr, Wenzel Jakob, and Greg Humphreys. This project aims to explore modern Rust features, and create a performant and stable rendering engine. + +This implementation is currently under development and serves as a learning exercise for both advanced rendering techniques and cutting-edge Rust programming. + +## Getting Started + +This project requires the Rust nightly toolchain + +To install the nightly toolchain: +```sh +rustup toolchain install nightly +rustup default nightly + +To get a local copy up and running, follow these simple steps. + +1. **Clone the repository:** + ```sh + git clone + cd pbrt + ``` + +2. **Build the project:** + ```sh + cargo build + ``` + +3. **Run the executable:** + ```sh + cargo run + ``` + +4. **Run tests:** + ```sh + cargo test + ``` + +## Dependencies + +This project relies on the following external crates: + +* [**bitflags**](https://crates.io/crates/bitflags) +* [**bumpalo**](https://crates.io/crates/bumpalo) +* [**num**](https://crates.io/crates/num) & [**num-traits**](https://crates.io/crates/num-traits) +* [**once_cell**](https://crates.io/crates/once_cell) +* [**rand**](https://crates.io/crates/rand) +* [**thiserror**](https://crates.io/crates/thiserror) + + +## Help + +Good luck. + +``` +command to run if program contains helper info +``` + +## Authors + +## Version History + +## License + +This project is licensed under the [NAME HERE] License - see the LICENSE.md file for details + +## Acknowledgments diff --git a/src/utils/error.rs b/src/utils/error.rs new file mode 100644 index 0000000..eb34411 --- /dev/null +++ b/src/utils/error.rs @@ -0,0 +1,84 @@ +use thiserror::Error; +use image::error; +use std::fmt; + +use crate::utils::image::PixelFormat; + +#[derive(Error, Debug)] +pub enum LlsError { + SingularMatrix, +} + +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum InversionError { + SingularMatrix, + EmptyMatrix, +} + +impl fmt::Display for LlsError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + LlsError::SingularMatrix => write!(f, "Matrix is singular and cannot be inverted."), + } + } +} + +impl fmt::Display for InversionError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + InversionError::SingularMatrix => { + write!(f, "Matrix is singular and cannot be inverted.") + } + InversionError::EmptyMatrix => write!(f, "Matrix is empty and cannot be inverted."), + } + } +} + +#[derive(Error, Debug)] +pub enum ImageError { + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + #[error("Image file error: {0}")] + Image(#[from] image::ImageError), + + #[error("EXR file error: {0}")] + Exr(#[from] exr::error::Error), + + #[error("QOI file error: {0}")] + Qoi(String), + + #[error("Pixel format error: {0}")] + PixelFormat(#[from] PixelFormatError), + + #[error("Unsupported operation: {0}")] + Unsupported(String), + + #[error("Mismatched dimensions or channels: {0}")] + Mismatch(String), + + #[error("Channel '{0}' not found in image")] + ChannelNotFound(String), +} + +/// Describes an error related to an unexpected or unsupported pixel format. +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum PixelFormatError { + #[error("Unsupported operation '{operation}' for pixel format {actual:?}. Reason: {reason}")] + UnsupportedOperation { + operation: String, + actual: PixelFormat, + reason: String, + }, + + #[error("Invalid conversion from pixel format {from:?} to {to:?}.")] + InvalidConversion { + from: PixelFormat, + to: PixelFormat, + }, + + #[error("Internal invariant violated: image format is {expected:?} but pixel data is of a different type.")] + InternalFormatMismatch { + expected: PixelFormat, + }, +} diff --git a/src/utils/image.rs b/src/utils/image.rs new file mode 100644 index 0000000..bef11a7 --- /dev/null +++ b/src/utils/image.rs @@ -0,0 +1,1100 @@ +use std::collections::HashMap; +use std::str::FromStr; +use std::fmt; +use std::path::Path; +use std::ops::{Index, IndexMut}; +use std::fs::{File, read}; +use std::io::{self, BufRead, BufReader, Write}; +use half::f16; +use rayon::prelude::*; +use crate::utils::colorspace::RGBColorspace; +use crate::utils::error::{ImageError, PixelFormatError}; +use crate::core::pbrt::{lerp, Float}; +use crate::utils::geometry::{Bounds2i, Point2f, Point2i}; +use crate::utils::color::{self, ColorEncoding, SRGBColorEncoding, LinearColorEncoding}; +use crate::utils::math::{SquareMatrix, windowed_sinc, gaussian}; +use image::{ImageBuffer, DynamicImage, Rgb, Rgba, Luma}; +use exr::prelude as exr_prelude; +use exr::prelude::*; +use exr::meta::header::Header; +use exr::image as exr_image; +use exr::math::Vec2; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PixelFormat { + U8, + F16, + F32, +} + +impl PixelFormat { + pub fn is_8bit(&self) -> bool { + matches!(self, PixelFormat::U8) + } + + pub fn is_16bit(&self) -> bool { + matches!(self, PixelFormat::F16) + } + + pub fn is_32bit(&self) -> bool { + matches!(self, PixelFormat::F32) + } + + pub fn texel_bytes(&self) -> usize { + match self { + PixelFormat::U8 => 1, + PixelFormat::F16 => 2, + PixelFormat::F32 => 4, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WrapMode { + Black, + Clamp, + Repeat, + OctahedralSphere, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct WrapMode2D { + pub uv: [WrapMode; 2], +} + +#[derive(Debug, Clone, Copy)] +pub struct ResampleWeight { + pub first_pixel: i32, + pub weight: [Float; 4], +} + +#[derive(Debug, Clone, Default)] +pub struct ImageChannelDesc { + pub offset: Vec, +} + +#[derive(Debug, Clone)] +pub struct ImageChannelValues(pub Vec); + +#[derive(Debug)] +pub struct ImageMetadata { + pub render_time_seconds: Option, + pub camera_from_world: Option>, + pub ndc_from_world: Option>, + pub pixel_bounds: Option, + pub full_resolution: Option, + pub samples_per_pixel: Option, + pub mse: Option, + pub colorspace: Option<&'static RGBColorspace>, + pub strings: HashMap, + pub string_vectors: HashMap>, +} + +#[derive(Debug, Clone)] +enum PixelData { + U8(Vec), + F16(Vec), + F32(Vec), +} + +#[derive(Debug)] +pub struct Image { + resolution: Point2i, + channel_names: Vec, + format: PixelFormat, + pixels: PixelData, + encoding: &'static dyn ColorEncoding, +} + +#[derive(Debug)] +pub struct ImageAndMetadata { + pub image: Image, + pub metadata: ImageMetadata, +} + +impl fmt::Display for PixelFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PixelFormat::U8 => write!(f, "U8"), + PixelFormat::F16 => write!(f, "F16 (Half)"), + PixelFormat::F32 => write!(f, "F32 (Float)"), + } + } +} + +impl fmt::Display for WrapMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WrapMode::Black => write!(f, "black"), + WrapMode::Clamp => write!(f, "clamp"), + WrapMode::Repeat => write!(f, "repeat"), + WrapMode::OctahedralSphere => write!(f, "octahedralsphere"), + } + } +} + +// impl FromStr for WrapMode { +// type Err = (); // Simple error type +// fn from_str(s: &str) -> Result { +// match s { +// "black" => Ok(WrapMode::Black), +// "clamp" => Ok(WrapMode::Clamp), +// "repeat" => Ok(WrapMode::Repeat), +// "octahedralsphere" => Ok(WrapMode::OctahedralSphere), +// _ => Err(()), +// } +// } +// } + +impl WrapMode2D { + pub fn new(u: WrapMode, v: WrapMode) -> Self { Self { uv: [u, v] } } +} + +impl From for WrapMode2D { + fn from(w: WrapMode) -> Self { Self { uv: [w, w] } } +} + +impl ImageChannelDesc { + pub fn len(&self) -> usize { self.offset.len() } + pub fn is_empty(&self) -> bool { self.offset.is_empty() } + pub fn is_identity(&self) -> bool { + self.offset.iter().enumerate().all(|(i, &off)| i == off) + } +} + +impl ImageChannelValues { + pub fn new(size: usize, value: Float) -> Self { Self(vec![value; size]) } + pub fn len(&self) -> usize { self.0.len() } + pub fn as_slice(&self) -> &[Float] { &self.0 } + pub fn max_value(&self) -> Float { + self.0.iter().fold(Float::NEG_INFINITY, |a, &b| a.max(b)) + } + pub fn average(&self) -> Float { + if self.0.is_empty() { 0.0 } else { self.0.iter().sum::() / self.0.len() as Float } + } +} + +impl Index for ImageChannelValues { + type Output = Float; + fn index(&self, index: usize) -> &Self::Output { &self.0[index] } +} + +impl IndexMut for ImageChannelValues { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { &mut self.0[index] } +} + +impl ImageMetadata { + pub fn new() -> Self { + Self { + render_time_seconds: None, + camera_from_world: None, + ndc_from_world: None, + pixel_bounds: None, + full_resolution: None, + samples_per_pixel: None, + mse: None, + colorspace: None, + strings: HashMap::new(), + string_vectors: HashMap::new(), + } + } + pub fn get_color_space(&self) -> &'static RGBColorspace { + self.colorspace.unwrap_or_else(|| RGBColorspace::default()) + } +} + +impl Default for ImageMetadata { + fn default() -> Self { + Self::new() + } +} + +impl fmt::Display for ImageMetadata { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut builder = f.debug_struct("ImageMetadata"); + if let Some(val) = self.render_time_seconds { + builder.field("render_time_seconds", &val); + } + if let Some(val) = &self.camera_from_world { + builder.field("camera_from_world", val); + } + if let Some(val) = &self.ndc_from_world { + builder.field("ndc_from_world", val); + } + if let Some(val) = &self.pixel_bounds { + builder.field("pixel_bounds", val); + } + if let Some(val) = &self.full_resolution { + builder.field("full_resolution", val); + } + if let Some(val) = self.samples_per_pixel { + builder.field("samples_per_pixel", &val); + } + if let Some(val) = self.mse { + builder.field("mse", &val); + } + builder.field("color_space", &self.get_color_space()); + if !self.strings.is_empty() { + builder.field("strings", &self.strings); + } + if !self.string_vectors.is_empty() { + builder.field("string_vectors", &self.string_vectors); + } + builder.finish() + } +} + +impl fmt::Display for Image { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[ Image format: {} resolution: {:?} channel_names: {:?} encoding: {:?} ]", self.format, self.resolution, self.channel_names, self.encoding.to_string()) + } +} + +impl fmt::Display for ImageChannelValues { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[ ImageChannelValues {:?} ]", self.0) + } +} + +impl fmt::Display for ImageChannelDesc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[ ImageChannelDesc offset: {:?} ]", self.offset) + } +} + +fn remap_pixel_coords(p: &mut Point2i, resolution: Point2i, wrap_mode: WrapMode2D) -> bool { + for i in 0..2 { + if p[i] >= 0 && p[i] < resolution[i] { + continue; + } + match wrap_mode.uv[i] { + WrapMode::Black => return false, + WrapMode::Clamp => p[i] = p[i].clamp(0, resolution[i] - 1), + WrapMode::Repeat => p[i] = p[i].rem_euclid(resolution[i]), + WrapMode::OctahedralSphere => { + p[i] = p[i].clamp(0, resolution[i] - 1); + } + } + } + true +} + +fn round_up_pow2(mut n: i32) -> i32 { + if n <= 0 { + return 1; + } + n -= 1; + n |= n >> 1; + n |= n >> 2; + n |= n >> 4; + n |= n >> 8; + n |= n >> 16; + n + 1 +} + +impl Image { + pub fn new( + format: PixelFormat, + resolution: Point2i, + channel_names: Vec, + encoding: &'static dyn ColorEncoding, + ) -> Self { + let n_elements = (resolution.x() * resolution.y()) as usize * channel_names.len(); + let pixels = match format { + PixelFormat::U8 => PixelData::U8(vec![0; n_elements]), + PixelFormat::F16 => PixelData::F16(vec![f16::from_f32(0.0); n_elements]), + PixelFormat::F32 => PixelData::F32(vec![0.0; n_elements]), + }; + Self { resolution, channel_names, pixels, format, encoding } + } + + // --- Accessors --- + pub fn format(&self) -> PixelFormat { self.format } + pub fn resolution(&self) -> Point2i { self.resolution } + pub fn n_channels(&self) -> usize { self.channel_names.len() } + pub fn channel_names(&self) -> &[String] { &self.channel_names } + pub fn encoding(&self) -> &'static dyn ColorEncoding { self.encoding } + + fn pixel_offset(&self, p: Point2i) -> usize { + assert!(p.x() >= 0 && p.x() < self.resolution.x()() && p.y() >= 0 && p.y() < self.resolution.y()()); + ((p.y() * self.resolution.x()() + p.x()) as usize) * self.n_channels() + } + + pub fn get_channel(&self, mut p: Point2i, c: usize, wrap_mode: WrapMode2D) -> Float { + if !remap_pixel_coords(&mut p, self.resolution, wrap_mode) { return 0.0; } + let offset = self.pixel_offset(p) + c; + match &self.pixels { + PixelData::U8(data) => { + let mut linear = [0.0]; + self.encoding.to_linear_slice(&data[offset..offset+1], &mut linear); + linear[0] + }, + PixelData::F16(data) => data[offset].to_f32(), + PixelData::F32(data) => data[offset], + } + } + + pub fn set_channel(&mut self, p: Point2i, c: usize, value: Float) { + let val_no_nan = if value.is_nan() { 0.0 } else { value }; + let offset = self.pixel_offset(p) + c; + match &mut self.pixels { + PixelData::U8(data) => { + let linear = [val_no_nan]; + self.encoding.from_linear_slice(&linear, &mut data[offset..offset+1]); + }, + PixelData::F16(data) => data[offset] = f16::from_f32(val_no_nan), + PixelData::F32(data) => data[offset] = val_no_nan, + } + } + + pub fn bilerp_channel(&self, p: Point2f, c: usize, wrap_mode: WrapMode2D) -> Float { + let x = p.x() * self.resolution.x()() as Float - 0.5; + let y = p.y() * self.resolution.y()() as Float - 0.5; + let xi = x.floor() as i32; let yi = y.floor() as i32; + let dx = x - xi as Float; let dy = y - yi as Float; + let v00 = self.get_channel(Point2i::new(xi, yi), c, wrap_mode); + let v10 = self.get_channel(Point2i::new(xi + 1, yi), c, wrap_mode); + let v01 = self.get_channel(Point2i::new(xi, yi + 1), c, wrap_mode); + let v11 = self.get_channel(Point2i::new(xi + 1, yi + 1), c, wrap_mode); + lerp(dy, lerp(dx, v00, v10), lerp(dx, v01, v11)) + } + + pub fn get_channels(&self, p: Point2i, desc: &ImageChannelDesc, wrap_mode: WrapMode2D) -> ImageChannelValues { + let mut cv = ImageChannelValues::new(desc.len(), 0.0); + let mut pp = p; + if !remap_pixel_coords(&mut pp, self.resolution, wrap_mode) { return cv; } + let offset = self.pixel_offset(pp); + match &self.pixels { + PixelData::U8(data) => { + for i in 0..desc.len() { + let mut linear = [0.0]; + self.encoding.to_linear_slice(&data[offset + desc.offset[i]..offset + desc.offset[i] + 1], &mut linear); + cv[i] = linear[0]; + } + }, + PixelData::F16(data) => { + for i in 0..desc.len() { + cv[i] = data[offset + desc.offset[i]].to_f32(); + } + }, + PixelData::F32(data) => { + for i in 0..desc.len() { + cv[i] = data[offset + desc.offset[i]]; + } + }, + } + cv + } + + pub fn get_channels_all(&self, p: Point2i, wrap_mode: WrapMode2D) -> ImageChannelValues { + self.get_channels(p, &self.all_channels_desc(), wrap_mode) + } + + pub fn set_channels(&mut self, p: Point2i, desc: &ImageChannelDesc, values: &ImageChannelValues) { + assert_eq!(desc.len(), values.len()); + for i in 0..desc.len() { + self.set_channel(p, desc.offset[i], values[i]); + } + } + + pub fn set_channels_all(&mut self, p: Point2i, values: &ImageChannelValues) { + self.set_channels(p, &self.all_channels_desc(), values) + } + + fn all_channels_desc(&self) -> ImageChannelDesc { + let mut desc = ImageChannelDesc::default(); + desc.offset = (0..self.n_channels()).collect(); + desc + } + + pub fn lookup_nearest_channel(&self, p: Point2f, c: usize, wrap_mode: WrapMode2D) -> Float { + let x = (p.x() * self.resolution.x()() as Float).round() as i32; + let y = (p.y() * self.resolution.y()() as Float).round() as i32; + self.get_channel(Point2i::new(x, y), c, wrap_mode) + } + + pub fn lookup_nearest(&self, p: Point2f, desc: &ImageChannelDesc, wrap_mode: WrapMode2D) -> ImageChannelValues { + let mut cv = ImageChannelValues::new(desc.len(), 0.0); + for i in 0..desc.len() { + cv[i] = self.lookup_nearest_channel(p, desc.offset[i], wrap_mode); + } + cv + } + + pub fn lookup_nearest_all(&self, p: Point2f, wrap_mode: WrapMode2D) -> ImageChannelValues { + self.lookup_nearest(p, &self.all_channels_desc(), wrap_mode) + } + + pub fn bilerp(&self, p: Point2f, desc: &ImageChannelDesc, wrap_mode: WrapMode2D) -> ImageChannelValues { + let mut cv = ImageChannelValues::new(desc.len(), 0.0); + for i in 0..desc.len() { + cv[i] = self.bilerp_channel(p, desc.offset[i], wrap_mode); + } + cv + } + + pub fn bilerp_all(&self, p: Point2f, wrap_mode: WrapMode2D) -> ImageChannelValues { + self.bilerp(p, &self.all_channels_desc(), wrap_mode) + } + + // --- Image Manipulation and Analysis --- + + pub fn get_channel_desc(&self, requested_channels: &[String]) -> Option { + let mut desc = ImageChannelDesc::default(); + for req_ch in requested_channels { + match self.channel_names.iter().position(|ch| ch == req_ch) { + Some(offset) => desc.offset.push(offset), + None => return None, + } + } + Some(desc) + } + + pub fn select_channels(&self, desc: &ImageChannelDesc) -> Image { + let new_channels: Vec = desc.offset.iter().map(|&i| self.channel_names[i].clone()).collect(); + let mut new_image = Image::new(self.format, self.resolution, new_channels, self.encoding); + let n_new_channels = new_image.n_channels(); + for y in 0..self.resolution.y() { + for x in 0..self.resolution.x() { + let p = Point2i::new(x, y); + let old_offset = self.pixel_offset(p); + let new_offset = new_image.pixel_offset(p); + match (&self.pixels, &mut new_image.pixels) { + (PixelData::U8(d_old), PixelData::U8(d_new)) => { + for c in 0..n_new_channels { d_new[new_offset + c] = d_old[old_offset + desc.offset[c]]; } + }, + (PixelData::F16(d_old), PixelData::F16(d_new)) => { + for c in 0..n_new_channels { d_new[new_offset + c] = d_old[old_offset + desc.offset[c]]; } + }, + (PixelData::F32(d_old), PixelData::F32(d_new)) => { + for c in 0..n_new_channels { d_new[new_offset + c] = d_old[old_offset + desc.offset[c]]; } + }, + _ => unreachable!(), + } + } + } + new_image + } + + pub fn crop(&self, bounds: Bounds2i) -> Image { + assert!(bounds.p_min.x() >= 0 && bounds.p_min.y() >= 0); + let new_res = Point2i::new(bounds.p_max.x() - bounds.p_min.x(), bounds.p_max.y() - bounds.p_min.y()); + let mut new_image = Image::new(self.format, new_res, self.channel_names.clone(), self.encoding); + for y in bounds.p_min.y()..bounds.p_max.y() { + for x in bounds.p_min.x()..bounds.p_max.x() { + let p_src = Point2i::new(x, y); + let p_dst = Point2i::new(x - bounds.p_min.x(), y - bounds.p_min.y()); + for c in 0..self.n_channels() { + new_image.set_channel(p_dst, c, self.get_channel(p_src, c, WrapMode::Clamp.into())); + } + } + } + new_image + } + + pub fn has_any_nan_pixels(&self) -> bool { + if self.format.is_8bit() { + return false; + } + (0..self.resolution.y()()) // Create a range for the y-axis + .into_par_iter() // Convert the range into a parallel iterator + .any(|y| { // Check in parallel if any row contains a NaN + (0..self.resolution.x()()) // For each row, create a sequential iterator for the x-axis + .any(|x| { + (0..self.n_channels()) // For each pixel, check each channel + .any(|c| self.get_channel(Point2i::new(x, y), c, WrapMode::Clamp.into()).is_nan()) + }) + }) + } + + pub fn has_any_infinite_pixels(&self) -> bool { + if self.format.is_8bit() { return false; } + (0..self.resolution.y()).into_par_iter().any(|y| { + (0..self.resolution.x()).any(|x| { + (0..self.n_channels()).any(|c| self.get_channel(Point2i::new(x, y), c, WrapMode::Clamp.into()).is_infinite()) + }) + }) + } + + pub fn flip_y(&mut self) { + let half_y = self.resolution.y() / 2; + for y in 0..half_y { + for x in 0..self.resolution.x() { + for c in 0..self.n_channels() { + let top = self.get_channel(Point2i::new(x, y), c, WrapMode::Clamp.into()); + let bottom = self.get_channel(Point2i::new(x, self.resolution.y()() - 1 - y), c, WrapMode::Clamp.into()); + self.set_channel(Point2i::new(x, y), c, bottom); + self.set_channel(Point2i::new(x, self.resolution.y() - 1 - y), c, top); + } + } + } + } + + pub fn convert_to_format(&self, new_format: PixelFormat, new_encoding: &'static dyn ColorEncoding) -> Result { + if self.format == new_format && self.encoding == new_encoding { + return Ok(self.clone()); + } + let mut new_image = Image::new(new_format, self.resolution, self.channel_names.clone(), new_encoding); + for y in 0..self.resolution.y() { + for x in 0..self.resolution.x() { + let p = Point2i::new(x, y); + for c in 0..self.n_channels() { + let val = self.get_channel(p, c, WrapMode::Clamp.into()); + new_image.set_channel(p, c, val); + } + } + } + Ok(new_image) + } + + pub fn gaussian_filter(&self, desc: &ImageChannelDesc, half_width: i32, sigma: Float) -> Image { + let mut wts: Vec = (0..=half_width * 2).map(|d| gaussian((d - half_width) as Float, 0.0, sigma)).collect(); + let wt_sum: Float = wts.iter().sum(); + wts.iter_mut().for_each(|w| *w /= wt_sum); + let mut blurx = Image::new(PixelFormat::F32, self.resolution, self.channel_names().to_vec(), self.encoding); + let nc = desc.len(); + (0..self.resolution.y()).into_par_iter().for_each(|y| { + for x in 0..self.resolution.x() { + let mut result = ImageChannelValues::new(desc.len(), 0.0); + for r in -half_width..=half_width { + let cv = self.get_channels(Point2i::new(x + r, y), desc, WrapMode::Clamp.into()); + let w = wts[(r + half_width) as usize]; + for c in 0..nc { + result[c] += w * cv[c]; + } + } + blurx.set_channels(Point2i::new(x, y), desc, &result); + } + }); + let mut blury = Image::new(PixelFormat::F32, self.resolution, self.channel_names().to_vec(), self.encoding); + (0..self.resolution.y()).into_par_iter().for_each(|y| { + for x in 0..self.resolution.x() { + let mut result = ImageChannelValues::new(desc.len(), 0.0); + for r in -half_width..=half_width { + let cv = blurx.get_channels(Point2i::new(x, y + r), desc, WrapMode::Clamp.into()); + let w = wts[(r + half_width) as usize]; + for c in 0..nc { + result[c] += w * cv[c]; + } + } + blury.set_channels(Point2i::new(x, y), desc, &result); + } + }); + blury + } + + pub fn joint_bilateral_filter(&self, to_filter_desc: &ImageChannelDesc, half_width: i32, xy_sigma: [Float; 2], joint_desc: &ImageChannelDesc, joint_sigma: &ImageChannelValues) -> Image { + assert_eq!(joint_desc.len(), joint_sigma.len()); + let result = &mut Image::new(PixelFormat::F32, self.resolution, self.channel_names().to_vec(), self.encoding); + let fx: Vec = (0..=half_width).map(|i| gaussian(i as Float, 0.0, xy_sigma[0])).collect(); + let fy: Vec = (0..=half_width).map(|i| gaussian(i as Float, 0.0, xy_sigma[1])).collect(); + (0..self.resolution.y()).into_par_iter().for_each(|y| { + for x in 0..self.resolution.x() { + let joint_pixel = self.get_channels(Point2i::new(x, y), joint_desc, WrapMode::Clamp.into()); + let mut filtered_sum = ImageChannelValues::new(to_filter_desc.len(), 0.0); + let mut weight_sum = 0.0; + for dy in -half_width..=half_width { + if y + dy < 0 || y + dy >= self.resolution.y() { continue; } + for dx in -half_width..=half_width { + if x + dx < 0 || x + dx >= self.resolution.x() { continue; } + let joint_other = self.get_channels(Point2i::new(x + dx, y + dy), joint_desc, WrapMode::Clamp.into()); + let mut weight = fx[dx.abs() as usize] * fy[dy.abs() as usize]; + for c in 0..joint_desc.len() { + weight *= gaussian(joint_pixel[c], joint_other[c], joint_sigma[c]); + } + weight_sum += weight; + let filter_channels = self.get_channels(Point2i::new(x + dx, y + dy), to_filter_desc, WrapMode::Clamp.into()); + for c in 0..to_filter_desc.len() { + filtered_sum[c] += weight * filter_channels[c]; + } + } + } + if weight_sum > 0.0 { + for c in 0..to_filter_desc.len() { + filtered_sum[c] /= weight_sum; + } + } + result.set_channels(Point2i::new(x, y), to_filter_desc, &filtered_sum); + } + }); + result.clone() + } + + pub fn mae(&self, desc: &ImageChannelDesc, ref_img: &Image, mut error_image: Option<&mut Image>) -> ImageChannelValues { + let mut sum_error = vec![0.0; desc.len()]; + let ref_desc = ref_img.get_channel_desc(&self.channel_names()).unwrap(); + assert_eq!(self.resolution, ref_img.resolution); + if let Some(err_img) = &mut error_image { + *err_img = Image::new(PixelFormat::F32, self.resolution, self.channel_names.clone(), self.encoding); + } + for y in 0..self.resolution.y() { + for x in 0..self.resolution.x() { + let v = self.get_channels(Point2i::new(x, y), desc, WrapMode::Clamp.into()); + let vref = ref_img.get_channels(Point2i::new(x, y), &ref_desc, WrapMode::Clamp.into()); + for c in 0..desc.len() { + let err = (v[c] - vref[c]).abs(); + if err.is_infinite() { continue; } + sum_error[c] += err; + if let Some(err_img) = &mut error_image { + err_img.set_channel(Point2i::new(x, y), c, err); + } + } + } + } + let mut mae = ImageChannelValues::new(desc.len(), 0.0); + let n_pixels = self.resolution.x()() as Float * self.resolution.y()() as Float; + for c in 0..desc.len() { + mae[c] = sum_error[c] / n_pixels; + } + mae + } + + pub fn mse(&self, desc: &ImageChannelDesc, ref_img: &Image, mut mse_image: Option<&mut Image>) -> ImageChannelValues { + let mut sum_se = vec![0.0; desc.len()]; + let ref_desc = ref_img.get_channel_desc(&self.channel_names(desc)).unwrap(); + assert_eq!(self.resolution, ref_img.resolution); + if let Some(mse_img) = &mut mse_image { + *mse_img = Image::new(PixelFormat::F32, self.resolution, self.channel_names.clone(), self.encoding); + } + for y in 0..self.resolution.y() { + for x in 0..self.resolution.x() { + let v = self.get_channels(Point2i::new(x, y), desc, WrapMode::Clamp.into()); + let vref = ref_img.get_channels(Point2i::new(x, y), &ref_desc, WrapMode::Clamp.into()); + for c in 0..desc.len() { + let se = (v[c] - vref[c]).powi(2); + if se.is_infinite() { continue; } + sum_se[c] += se; + if let Some(mse_img) = &mut mse_image { + mse_img.set_channel(Point2i::new(x, y), c, se); + } + } + } + } + let mut mse = ImageChannelValues::new(desc.len(), 0.0); + let n_pixels = self.resolution.x() as Float * self.resolution.y() as Float; + for c in 0..desc.len() { + mse[c] = sum_se[c] / n_pixels; + } + mse + } + + pub fn mrse(&self, desc: &ImageChannelDesc, ref_img: &Image, mut mrse_image: Option<&mut Image>) -> ImageChannelValues { + let mut sum_rse = vec![0.0; desc.len()]; + let ref_desc = ref_img.get_channel_desc(&self.channel_names()).unwrap(); + assert_eq!(self.resolution, ref_img.resolution); + if let Some(mrse_img) = &mut mrse_image { + *mrse_img = Image::new(PixelFormat::F32, self.resolution, self.channel_names.clone(), self.encoding); + } + for y in 0..self.resolution.y() { + for x in 0..self.resolution.x() { + let v = self.get_channels(Point2i::new(x, y), desc, WrapMode::Clamp.into()); + let vref = ref_img.get_channels(Point2i::new(x, y), &ref_desc, WrapMode::Clamp.into()); + for c in 0..desc.len() { + let rse = ((v[c] - vref[c]).powi(2)) / (vref[c] + 0.01).powi(2); + if rse.is_infinite() { continue; } + sum_rse[c] += rse; + if let Some(mrse_img) = &mut mrse_image { + mrse_img.set_channel(Point2i::new(x, y), c, rse); + } + } + } + } + let mut mrse = ImageChannelValues::new(desc.len(), 0.0); + let n_pixels = self.resolution.x() as Float * self.resolution.y() as Float; + for c in 0..desc.len() { + mrse[c] = sum_rse[c] / n_pixels; + } + mrse + } + + pub fn average(&self, desc: &ImageChannelDesc) -> ImageChannelValues { + let mut sum = vec![0.0; desc.len()]; + for y in 0..self.resolution.y() { + for x in 0..self.resolution.x() { + let v = self.get_channels(Point2i::new(x, y), desc, WrapMode::Clamp.into()); + for c in 0..desc.len() { + sum[c] += v[c]; + } + } + } + let mut avg = ImageChannelValues::new(desc.len(), 0.0); + let n_pixels = self.resolution.x() as Float * self.resolution.y() as Float; + for c in 0..desc.len() { + avg[c] = sum[c] / n_pixels; + } + avg + } + + pub fn copy_rect_out(&self, extent: Bounds2i, buf: &mut [Float], wrap_mode: WrapMode2D) { + assert_eq!(buf.len(), (extent.p_max.x() - extent.p_min.x()) as usize * (extent.p_max.y() - extent.p_min.y()) as usize * self.n_channels()); + let mut buf_idx = 0; + for y in extent.p_min.y()..extent.p_max.y() { + for x in extent.p_min.x()..extent.p_max.x() { + for c in 0..self.n_channels() { + buf[buf_idx] = self.get_channel(Point2i::new(x, y), c, wrap_mode); + buf_idx += 1; + } + } + } + } + + pub fn copy_rect_in(&mut self, extent: Bounds2i, buf: &[Float]) { + assert_eq!(buf.len(), (extent.p_max.x() - extent.p_min.x()) as usize * (extent.p_max.y() - extent.p_min.y()) as usize * self.n_channels()); + let mut buf_idx = 0; + for y in extent.p_min.y()..extent.p_max.y() { + for x in extent.p_min.x()..extent.p_max.x() { + for c in 0..self.n_channels() { + self.set_channel(Point2i::new(x, y), c, buf[buf_idx]); + buf_idx += 1; + } + } + } + } + + // --- Resampling and Pyramid Generation --- + + fn resample_weights(old_res: i32, new_res: i32) -> Vec { + assert!(new_res >= old_res); + let mut wt = vec![ResampleWeight { first_pixel: 0, weight: [0.0; 4] }; new_res as usize]; + let filter_radius = 2.0; + let tau = 2.0; + for i in 0..new_res { + let center = (i as Float + 0.5) * old_res as Float / new_res as Float; + wt[i as usize].first_pixel = ((center - filter_radius) + 0.5).floor() as i32; + let mut sum_wts = 0.0; + for j in 0..4 { + let pos = wt[i as usize].first_pixel as Float + j as Float + 0.5; + let w = windowed_sinc(pos - center, filter_radius, tau); + wt[i as usize].weight[j] = w; + sum_wts += w; + } + if sum_wts > 0.0 { + for j in 0..4 { + wt[i as usize].weight[j] /= sum_wts; + } + } + } + wt + } + + pub fn float_resize_up(&self, new_res: Point2i, wrap_mode: WrapMode2D) -> Image { + assert!(self.format.is_32bit()); + let mut temp_image = Image::new(PixelFormat::F32, Point2i::new(new_res.x(), self.resolution.y()), self.channel_names.clone(), self.encoding); + let x_weights = Self::resample_weights(self.resolution.x(), new_res.x()); + (0..self.resolution.y()).into_par_iter().for_each(|y| { + for x_out in 0..new_res.x() { + let weights = &x_weights[x_out as usize]; + for c in 0..self.n_channels() { + let mut val = 0.0; + for j in 0..4 { + let px = (weights.first_pixel + j as i32).clamp(0, self.resolution.x() - 1); + val += weights.weight[j] * self.get_channel(Point2i::new(px, y), c, wrap_mode); + } + temp_image.set_channel(Point2i::new(x_out, y), c, val.max(0.0)); + } + } + }); + let mut new_image = Image::new(PixelFormat::F32, new_res, self.channel_names.clone(), self.encoding); + let y_weights = Self::resample_weights(self.resolution.y(), new_res.y()); + (0..new_res.x()).into_par_iter().for_each(|x| { + for y_out in 0..new_res.y() { + let weights = &y_weights[y_out as usize]; + for c in 0..self.n_channels() { + let mut val = 0.0; + for j in 0..4 { + let py = (weights.first_pixel + j as i32).clamp(0, self.resolution.y() - 1); + val += weights.weight[j] * temp_image.get_channel(Point2i::new(x, py), c, wrap_mode); + } + new_image.set_channel(Point2i::new(x, y_out), c, val.max(0.0)); + } + } + }); + new_image + } + + pub fn generate_pyramid(image: Image, wrap_mode: WrapMode2D) -> Vec { + let mut img = image; + if !img.resolution.x() % 2 == 0 || !img.resolution.y() % 2 == 0 { + let new_res = Point2i::new(round_up_pow2(img.resolution.x()), round_up_pow2(img.resolution.y())); + img = img.float_resize_up(new_res, wrap_mode); + } + if !img.format.is_32bit() { + img = img.convert_to_format(PixelFormat::F32, color::LINEAR_SRGB).unwrap(); + } + let mut pyramid = Vec::new(); + let n_levels = 1 + img.resolution.x().max(img.resolution.y()).ilog2() as usize; + pyramid.reserve(n_levels); + pyramid.push(img.clone()); + for _ in 1..n_levels { + let res = img.resolution(); + let next_res = Point2i::new((res.x() / 2).max(1), (res.y() / 2).max(1)); + let mut next_image = Image::new(PixelFormat::F32, next_res, img.channel_names.clone(), img.encoding); + (0..next_res.y()).into_par_iter().for_each(|y| { + for x in 0..next_res.x() { + for c in 0..img.n_channels() { + let val = ( img.get_channel(Point2i::new(2 * x, 2 * y), c, wrap_mode) + + img.get_channel(Point2i::new(2 * x + 1, 2 * y), c, wrap_mode) + + img.get_channel(Point2i::new(2 * x, 2 * y + 1), c, wrap_mode) + + img.get_channel(Point2i::new(2 * x + 1, 2 * y + 1), c, wrap_mode) ) * 0.25; + next_image.set_channel(Point2i::new(x, y), c, val); + } + } + }); + pyramid.push(next_image.clone()); + img = next_image; + } + pyramid + } + + pub fn read(filename: &str, encoding: Option<&'static dyn ColorEncoding>) -> Result { + let path = Path::new(filename); + let ext = path.extension().and_then(|s| s.to_str()).unwrap_or(""); + match ext.to_lowercase().as_str() { + "exr" => Self::read_exr(filename), + "png" => Self::read_png(filename, encoding.unwrap_or(color::SRGB)), + "pfm" => Self::read_pfm(filename), + "hdr" => Self::read_hdr(filename), + "qoi" => Self::read_qoi(filename), + _ => { + // Use image crate for other formats + let img = image::open(filename).map_err(|e| ImageError::Io(e.into()))?; + Self::from_dynamic_image(img) + } + } + } + + fn read_exr(filename: &str) -> Result { + // Use exr crate to read + let image = exr_image::read::read().no_deep_data().largest_resolution_level().all_channels().all_layers().from_file(filename).map_err(|e| ImageError::ExrError(e.to_string()))?; + // Assume single layer, RGBA or RGB + if let exr_image::Image { layers: vec![layer], .. } = image { + let res = layer.channel_data.resolution; + let resolution = Point2i { x: res.0 as i32, y: res.1 as i32 }; + let mut channel_names = Vec::new(); + let mut pixels_f32 = Vec::new(); + for channel in &layer.channel_data.list { + channel_names.push(channel.name.to_string()); + if let exr_image::Channel { sample_data: exr_image::Samples::F32(data), .. } = channel { + pixels_f32.extend_from_slice(data); + } else { + return Err(ImageError::PixelFormatError("Unsupported EXR pixel type".to_string())); + } + } + let image = Image::new(PixelFormat::F32, resolution, channel_names, color::LINEAR_SRGB); + if let PixelData::F32(ref mut pd) = image.pixels { + *pd = pixels_f32; + } + let mut metadata = ImageMetadata::new(); + if let Some(cs) = layer.attributes.chromaticities { + // Set colorspace from chromaticities + metadata.colorspace = Some(RGBColorspace::from_chromaticities(cs.red.into(), cs.green.into(), cs.blue.into(), cs.white.into())); + } + // Add other metadata from header + Ok(ImageAndMetadata { image, metadata }) + } else { + Err(ImageError::ExrError("Multi-layer EXR not supported".to_string())) + } + } + + fn read_png(filename: &str, encoding: &'static dyn ColorEncoding) -> Result { + let img = image::open(filename).map_err(|e| ImageError::Io(e.into()))?; + Self::from_dynamic_image(img) + } + + fn read_pfm(filename: &str) -> Result { + // Custom PFM read + let file = File::open(filename).map_err(ImageError::Io)?; + let reader = BufReader::new(file); + let mut lines = reader.lines(); + let header = lines.next().ok_or(ImageError::Io(io::Error::new(io::ErrorKind::InvalidData, "No header")))? .map_err(ImageError::Io)?; + let n_channels = if header == "PF" { 3 } else if header == "Pf" { 1 } else { return Err(ImageError::PixelFormatError("Invalid PFM header".to_string())); }; + let dims = lines.next().ok_or(ImageError::Io(io::Error::new(io::ErrorKind::InvalidData, "No dims")))? .map_err(ImageError::Io)?; + let mut dims_iter = dims.split_whitespace(); + let width = dims_iter.next().unwrap().parse::().unwrap(); + let height = dims_iter.next().unwrap().parse::().unwrap(); + let scale = lines.next().ok_or(ImageError::Io(io::Error::new(io::ErrorKind::InvalidData, "No scale")))? .map_err(ImageError::Io)?.parse::().unwrap(); + // Read binary data + let data = read(filename).map_err(ImageError::Io)?; + let header_len = header.len() + dims.len() + scale.to_string().len() + 3; // + newlines + let mut pixels = vec![0.0; (width * height * n_channels) as usize]; + let byte_data = &data[header_len..]; + let mut byte_iter = byte_data.chunks(4); + for p in pixels.iter_mut() { + let bytes = byte_iter.next().unwrap(); + *p = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) * scale.abs(); + } + let channel_names = if n_channels == 1 { vec!["Y".to_string()] } else { vec!["R".to_string(), "G".to_string(), "B".to_string()] }; + let image = Image::new(PixelFormat::F32, Point2i { x: width, y: height }, channel_names, color::LINEAR_SRGB); + if let PixelData::F32(ref mut pd) = image.pixels { + *pd = pixels; + } + Ok(ImageAndMetadata { image, metadata: ImageMetadata::new() }) + } + + fn read_hdr(filename: &str) -> Result { + let img = image::open(filename).map_err(|e| ImageError::Io(e.into()))?; + Self::from_dynamic_image(img) + } + + fn read_qoi(filename: &str) -> Result { + // Assume qoi crate added; pseudo + let data = read(filename).map_err(ImageError::Io)?; + let (header, pixels) = qoi::decode(&data).map_err(|e| ImageError::QoiError(e.to_string()))?; + let resolution = Point2i::new(header.width as i32, header.height as i32); + let channel_names = if header.channels == 3 { vec!["R".to_string(), "G".to_string(), "B".to_string()] } else { vec!["R".to_string(), "G".to_string(), "B".to_string(), "A".to_string()] }; + let encoding = if header.colorspace == qoi::ColorSpace::Srgb { color::SRGB } else { color::LINEAR_SRGB }; + let image = Image::new(PixelFormat::U8, resolution, channel_names, encoding); + if let PixelData::U8(ref mut pd) = image.pixels { + *pd = pixels; + } + Ok(ImageAndMetadata { image, metadata: ImageMetadata::new() }) + } + + fn from_dynamic_image(img: DynamicImage) -> Result { + let resolution = Point2i::new(img.width() as i32, img.height() as i32); + let (pixels, channel_names) = match img { + DynamicImage::ImageLuma8(buf) => (PixelData::U8(buf.into_vec()), vec!["Y".to_string()] ), + DynamicImage::ImageRgb8(buf) => (PixelData::U8(buf.into_vec()), vec!["R".to_string(), "G".to_string(), "B".to_string()] ), + DynamicImage::ImageRgba8(buf) => (PixelData::U8(buf.into_vec()), vec!["R".to_string(), "G".to_string(), "B".to_string(), "A".to_string()] ), + DynamicImage::ImageLuma16(buf) => (PixelData::F16(buf.into_vec().into_iter().map(|u| f16::from_f32(u as f32 / 65535.0)).collect()), vec!["Y".to_string()] ), + DynamicImage::ImageRgb16(buf) => (PixelData::F16(buf.into_vec().into_iter().map(|u| f16::from_f32(u as f32 / 65535.0)).collect()), vec!["R".to_string(), "G".to_string(), "B".to_string()] ), + DynamicImage::ImageRgba16(buf) => (PixelData::F16(buf.into_vec().into_iter().map(|u| f16::from_f32(u as f32 / 65535.0)).collect()), vec!["R".to_string(), "G".to_string(), "B".to_string(), "A".to_string()] ), + DynamicImage::ImageRgb32F(buf) => (PixelData::F32(buf.into_vec()), vec!["R".to_string(), "G".to_string(), "B".to_string()] ), + _ => return Err(ImageError::PixelFormatError("Unsupported image format".to_string())), + }; + let format = match &pixels { + PixelData::U8(_) => PixelFormat::U8, + PixelData::F16(_) => PixelFormat::F16, + PixelData::F32(_) => PixelFormat::F32, + }; + let image = Image { resolution, channel_names, format, pixels, encoding: color::SRGB }; + Ok(ImageAndMetadata { image, metadata: ImageMetadata::new() }) + } + + pub fn write(&self, filename: &str, metadata: &ImageMetadata) -> Result<(), ImageError> { + let path = Path::new(filename); + let ext = path.extension().and_then(|s| s.to_str()).unwrap_or(""); + match ext.to_lowercase().as_str() { + "exr" => self.write_exr(filename, metadata), + "png" => self.write_png(filename, metadata), + "pfm" => self.write_pfm(filename, metadata), + "qoi" => self.write_qoi(filename, metadata), + _ => Err(ImageError::Io(io::Error::new(io::ErrorKind::InvalidInput, "Unsupported format"))), + } + } + + fn write_exr(&self, filename: &str, metadata: &ImageMetadata) -> Result<(), ImageError> { + // Use exr crate + let mut headers = Vec::new(); + let mut channels = Vec::new(); + for (i, name) in self.channel_names.iter().enumerate() { + channels.push(exr_image::Channel::new(name.clone(), exr_image::SampleType::F32)); + } + let layer = exr_image::Layer::new(Vec2(self.resolution.x() as usize, self.resolution.y() as usize), channels); + let mut header = Header::new(layer.resolution); + // Add metadata + if let Some(cs) = metadata.colorspace { + let chr = cs.to_chromaticities(); + header.chromaticities = Some(exr::meta::attribute::Chromaticities::new(chr.red.into(), chr.green.into(), chr.blue.into(), chr.white.into())); + } + headers.push(header); + let image = exr_image::Image::new(headers); + let pixels = if let PixelData::F32(data) = &self.pixels { data.clone() } else { return Err(ImageError::PixelFormatError("EXR write requires F32".to_string())); }; + image.write_to_file(filename, &pixels).map_err(|e| ImageError::ExrError(e.to_string()))?; + Ok(()) + } + + fn write_png(&self, filename: &str, metadata: &ImageMetadata) -> Result<(), ImageError> { + let mut buf = Vec::new(); + match self.format { + PixelFormat::U8 => { + if let PixelData::U8(data) = &self.pixels { + buf = data.clone(); + } + }, + _ => { + // Quantize to U8 + let mut data_u8 = vec![0u8; self.resolution.x() as usize * self.resolution.y() as usize * self.n_channels()]; + let mut out_of_gamut = 0; + for y in 0..self.resolution.y() { + for x in 0..self.resolution.x() { + for c in 0..self.n_channels() { + let v = self.get_channel(Point2i::new(x, y), c, WrapMode::Clamp.into()); + if v < 0.0 || v > 1.0 { out_of_gamut += 1; } + let v_clamp = v.clamp(0.0, 1.0); + data_u8[(y * self.resolution.x() + x) as usize * self.n_channels() + c] = (v_clamp * 255.0) as u8; + } + } + } + if out_of_gamut > 0 { + println!("Warning: {} out of gamut values in PNG write", out_of_gamut); + } + buf = data_u8; + } + } + let img_buf = match self.n_channels() { + 1 => ImageBuffer::, Vec>::from_vec(self.resolution.x() as u32, self.resolution.y() as u32, buf).unwrap(), + 3 => ImageBuffer::, Vec>::from_vec(self.resolution.x() as u32, self.resolution.y() as u32, buf).unwrap(), + 4 => ImageBuffer::, Vec>::from_vec(self.resolution.x() as u32, self.resolution.y() as u32, buf).unwrap(), + _ => return Err(ImageError::PixelFormatError("Unsupported channels for PNG".to_string())), + }; + img_buf.save(filename).map_err(|e| ImageError::Io(e.into()))?; + Ok(()) + } + + fn write_pfm(&self, filename: &str, metadata: &ImageMetadata) -> Result<(), ImageError> { + let mut file = File::create(filename).map_err(ImageError::Io)?; + let header = if self.n_channels() == 1 { "Pf\n" } else { "PF\n" }; + file.write_all(header.as_bytes()).map_err(ImageError::Io)?; + write!(file, "{} {}\n-1.0\n", self.resolution.x(), self.resolution.y()).map_err(ImageError::Io)?; + for y in (0..self.resolution.y()).rev() { + for x in 0..self.resolution.x() { + for c in 0..self.n_channels() { + let v = self.get_channel(Point2i::new(x, y), c, WrapMode::Clamp.into()); + file.write_all(&v.to_le_bytes()).map_err(ImageError::Io)?; + } + } + } + Ok(()) + } + + fn write_qoi(&self, filename: &str, metadata: &ImageMetadata) -> Result<(), ImageError> { + // Assume qoi crate; pseudo + let mut data_u8 = vec![0u8; self.resolution.x() as usize * self.resolution.y() as usize * self.n_channels()]; + let mut out_of_gamut = 0; + for y in 0..self.resolution.y() { + for x in 0..self.resolution.x() { + for c in 0..self.n_channels() { + let v = self.get_channel(Point2i::new(x, y), c, WrapMode::Clamp.into()); + if v < 0.0 || v > 1.0 { out_of_gamut += 1; } + let v_clamp = v.clamp(0.0, 1.0); + data_u8[(y * self.resolution.x() + x) as usize * self.n_channels() + c] = (v_clamp * 255.0) as u8; + } + } + } + if out_of_gamut > 0 { + println!("Warning: {} out of gamut values in QOI write", out_of_gamut); + } + let header = qoi::Header::new(self.resolution.x() as u32, self.resolution.y() as u32, self.n_channels() as u8, qoi::ColorSpace::Srgb); + let encoded = qoi::encode(&data_u8, &header).map_err(|e| ImageError::QoiError(e.to_string()))?; + std::fs::write(filename, encoded).map_err(ImageError::Io)?; + Ok(()) + } +} + +impl Clone for Image { + fn clone(&self) -> Self { + let pixels = match &self.pixels { + PixelData::U8(d) => PixelData::U8(d.clone()), + PixelData::F16(d) => PixelData::F16(d.clone()), + PixelData::F32(d) => PixelData::F32(d.clone()), + }; + Self { + resolution: self.resolution, + channel_names: self.channel_names.clone(), + format: self.format, + pixels, + encoding: self.encoding, + } + } +}