Skip to content

Commit

Permalink
Added functions for converting image colorspace.
Browse files Browse the repository at this point in the history
  • Loading branch information
Cykooz committed Aug 31, 2022
1 parent 9b8bba2 commit c454c3b
Show file tree
Hide file tree
Showing 12 changed files with 760 additions and 18 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -21,6 +21,7 @@ exclude = ["/data"]

[dependencies]
num-traits = "0.2.15"
once_cell = "1.13.0"
thiserror = "1.0.31"


Expand Down
41 changes: 32 additions & 9 deletions resizer/src/main.rs
@@ -1,7 +1,6 @@
use std::ffi::OsStr;
use std::fmt::Debug;
use std::num::NonZeroU32;
use std::path::{Path, PathBuf};
use std::path::PathBuf;

use anyhow::{anyhow, Context, Result};
use clap::Parser;
Expand Down Expand Up @@ -37,6 +36,10 @@ struct Cli {
#[clap(short, long, action)]
overwrite: bool,

/// Colorspace of image
#[clap(short, long, value_enum, default_value_t = structs::ColorSpace::NonLinear)]
colorspace: structs::ColorSpace,

/// Algorithm used to resize image
#[clap(short, long, value_enum, default_value_t = structs::Algorithm::Convolution)]
algorithm: structs::Algorithm,
Expand All @@ -58,7 +61,7 @@ fn main() -> Result<()> {
}

fn resize(cli: &Cli) -> Result<()> {
let (mut src_image, color_type) = open_source_image(&cli.source_path)?;
let (mut src_image, color_type) = open_source_image(cli)?;
let mut dst_image = create_destination_image(cli, &src_image);

let mul_div = fr::MulDiv::default();
Expand Down Expand Up @@ -88,13 +91,11 @@ fn resize(cli: &Cli) -> Result<()> {
.with_context(|| "Failed to divide color channels by alpha")?;
}

save_result(cli, &dst_image, color_type)
save_result(cli, dst_image, color_type)
}

fn open_source_image<P>(source_path: P) -> Result<(fr::Image<'static>, ColorType)>
where
P: AsRef<Path> + Debug,
{
fn open_source_image(cli: &Cli) -> Result<(fr::Image<'static>, ColorType)> {
let source_path = &cli.source_path;
debug!("Opening the source image {:?}", source_path);
let image = ImageReader::open(&source_path)
.with_context(|| format!("Failed to read source file from {:?}", source_path))?
Expand Down Expand Up @@ -195,7 +196,7 @@ fn get_resizing_algorithm(cli: &Cli) -> fr::ResizeAlg {
}
}

fn save_result(cli: &Cli, image: &fr::Image, color_type: ColorType) -> Result<()> {
fn save_result(cli: &Cli, mut image: fr::Image, color_type: ColorType) -> Result<()> {
let result_path = if let Some(path) = cli.destination_path.clone() {
path
} else {
Expand All @@ -214,6 +215,28 @@ fn save_result(cli: &Cli, image: &fr::Image, color_type: ColorType) -> Result<()
result_path
));
};

image = match cli.colorspace {
structs::ColorSpace::NonLinear => {
debug!("Convert the result image from linear colorspace into non-linear");
let mut non_linear_dst_image =
fr::Image::new(image.width(), image.height(), image.pixel_type());
if color_type.has_color() {
fr::color::srgb::rgb_into_srgb(
&image.view(),
&mut non_linear_dst_image.view_mut(),
)?;
} else {
fr::color::gamma::linear_into_gamma22(
&image.view(),
&mut non_linear_dst_image.view_mut(),
)?;
}
non_linear_dst_image
}
_ => image,
};

debug!("Save the result image into the file {:?}", result_path);
image::save_buffer(
result_path,
Expand Down
7 changes: 7 additions & 0 deletions resizer/src/structs.rs
Expand Up @@ -48,3 +48,10 @@ impl From<FilterType> for fr::FilterType {
}
}
}

#[derive(Copy, Clone, Debug, clap::ValueEnum)]
pub enum ColorSpace {
Linear,
/// sRGB for color images or gamma 2.2 for grayscale images
NonLinear,
}
198 changes: 198 additions & 0 deletions src/color/gamma.rs
@@ -0,0 +1,198 @@
//! Functions for changing image gamma.
use once_cell::sync::Lazy;

use crate::pixels::{U16x2, U16x3, U16x4, U8x2, U8x3, U8x4, U16, U8};
use crate::typed_image_view::{TypedImageView, TypedImageViewMut};
use crate::{ImageView, ImageViewMut, MappingError, PixelType};

use super::MappingTable;

macro_rules! gamma_table {
($src_type:tt, $dst_type:tt, $gamma:expr) => {{
const TABLE_SIZE: usize = $src_type::MAX as usize + 1;
let mut table: [$dst_type; TABLE_SIZE] = [0; TABLE_SIZE];
table.iter_mut().enumerate().for_each(|(i, v)| {
let signal = i as f32 / $src_type::MAX as f32;
let power = signal.powf($gamma);
*v = ($dst_type::MAX as f32 * power).round() as $dst_type;
});
table
}};
}

static GAMMA22_U8_INTO_LINEAR_U8: Lazy<MappingTable<u8, 256>> =
Lazy::new(|| MappingTable(gamma_table!(u8, u8, 2.2)));
static LINEAR_U8_INTO_GAMMA22_U8: Lazy<MappingTable<u8, 256>> =
Lazy::new(|| MappingTable(gamma_table!(u8, u8, 1.0 / 2.2)));
static GAMMA22_U8_INTO_LINEAR_U16: Lazy<MappingTable<u16, 256>> =
Lazy::new(|| MappingTable(gamma_table!(u8, u16, 2.2)));
static LINEAR_U8_INTO_GAMMA22_U16: Lazy<MappingTable<u16, 256>> =
Lazy::new(|| MappingTable(gamma_table!(u8, u16, 1.0 / 2.2)));
static GAMMA22_U16_INTO_LINEAR_U8: Lazy<MappingTable<u8, 65536>> =
Lazy::new(|| MappingTable(gamma_table!(u16, u8, 2.2)));
static LINEAR_U16_INTO_GAMMA22_U8: Lazy<MappingTable<u8, 65536>> =
Lazy::new(|| MappingTable(gamma_table!(u16, u8, 1.0 / 2.2)));
static GAMMA22_U16_INTO_LINEAR_U16: Lazy<MappingTable<u16, 65536>> =
Lazy::new(|| MappingTable(gamma_table!(u16, u16, 2.2)));
static LINEAR_U16_INTO_GAMMA22_U16: Lazy<MappingTable<u16, 65536>> =
Lazy::new(|| MappingTable(gamma_table!(u16, u16, 1.0 / 2.2)));

/// Convert image from gamma 2.2 into linear colorspace.
pub fn gamma22_into_linear(
src_image: &ImageView,
dst_image: &mut ImageViewMut,
) -> Result<(), MappingError> {
if src_image.width() != dst_image.width() || src_image.height() != dst_image.height() {
return Err(MappingError::DifferentDimensions);
}

macro_rules! map {
($src_pixel:ty, $dst_pixel:ty, $mapping_table:ident) => {
if let Some(src) = TypedImageView::<$src_pixel>::from_image_view(src_image) {
if let Some(dst) = TypedImageViewMut::<$dst_pixel>::from_image_view(dst_image) {
$mapping_table.map_typed_image(src, dst);
}
}
};
}

let src_pixel_type = src_image.pixel_type();
let dst_pixel_type = dst_image.pixel_type();
match (src_pixel_type, dst_pixel_type) {
// U8 -> U8
(PixelType::U8, PixelType::U8) => {
map!(U8, U8, GAMMA22_U8_INTO_LINEAR_U8);
}
(PixelType::U8x2, PixelType::U8x2) => {
map!(U8x2, U8x2, GAMMA22_U8_INTO_LINEAR_U8);
}
(PixelType::U8x3, PixelType::U8x3) => {
map!(U8x3, U8x3, GAMMA22_U8_INTO_LINEAR_U8);
}
(PixelType::U8x4, PixelType::U8x4) => {
map!(U8x4, U8x4, GAMMA22_U8_INTO_LINEAR_U8);
}
// U8 -> U16
(PixelType::U8, PixelType::U16) => {
map!(U8, U16, GAMMA22_U8_INTO_LINEAR_U16);
}
(PixelType::U8x2, PixelType::U16x2) => {
map!(U8x2, U16x2, GAMMA22_U8_INTO_LINEAR_U16);
}
(PixelType::U8x3, PixelType::U16x3) => {
map!(U8x3, U16x3, GAMMA22_U8_INTO_LINEAR_U16);
}
(PixelType::U8x4, PixelType::U16x4) => {
map!(U8x4, U16x4, GAMMA22_U8_INTO_LINEAR_U16);
}
// U16 -> U8
(PixelType::U16, PixelType::U8) => {
map!(U16, U8, GAMMA22_U16_INTO_LINEAR_U8);
}
(PixelType::U16x2, PixelType::U8x2) => {
map!(U16x2, U8x2, GAMMA22_U16_INTO_LINEAR_U8);
}
(PixelType::U16x3, PixelType::U8x3) => {
map!(U16x3, U8x3, GAMMA22_U16_INTO_LINEAR_U8);
}
(PixelType::U16x4, PixelType::U8x4) => {
map!(U16x4, U8x4, GAMMA22_U16_INTO_LINEAR_U8);
}
// U16 -> U16
(PixelType::U16, PixelType::U16) => {
map!(U16, U16, GAMMA22_U16_INTO_LINEAR_U16);
}
(PixelType::U16x2, PixelType::U16x2) => {
map!(U16x2, U16x2, GAMMA22_U16_INTO_LINEAR_U16);
}
(PixelType::U16x3, PixelType::U16x3) => {
map!(U16x3, U16x3, GAMMA22_U16_INTO_LINEAR_U16);
}
(PixelType::U16x4, PixelType::U16x4) => {
map!(U16x4, U16x4, GAMMA22_U16_INTO_LINEAR_U16);
}
_ => return Err(MappingError::UnsupportedCombinationOfImageTypes),
}

Ok(())
}

/// Convert image from linear colorspace into gamma 2.2.
pub fn linear_into_gamma22(
src_image: &ImageView,
dst_image: &mut ImageViewMut,
) -> Result<(), MappingError> {
if src_image.width() != dst_image.width() || src_image.height() != dst_image.height() {
return Err(MappingError::DifferentDimensions);
}

macro_rules! map {
($src_pixel:ty, $dst_pixel:ty, $mapping_table:ident) => {
if let Some(src) = TypedImageView::<$src_pixel>::from_image_view(src_image) {
if let Some(dst) = TypedImageViewMut::<$dst_pixel>::from_image_view(dst_image) {
$mapping_table.map_typed_image(src, dst);
}
}
};
}

let src_pixel_type = src_image.pixel_type();
let dst_pixel_type = dst_image.pixel_type();
match (src_pixel_type, dst_pixel_type) {
// U8 -> U8
(PixelType::U8, PixelType::U8) => {
map!(U8, U8, LINEAR_U8_INTO_GAMMA22_U8);
}
(PixelType::U8x2, PixelType::U8x2) => {
map!(U8x2, U8x2, LINEAR_U8_INTO_GAMMA22_U8);
}
(PixelType::U8x3, PixelType::U8x3) => {
map!(U8x3, U8x3, LINEAR_U8_INTO_GAMMA22_U8);
}
(PixelType::U8x4, PixelType::U8x4) => {
map!(U8x4, U8x4, LINEAR_U8_INTO_GAMMA22_U8);
}
// U8 -> U16
(PixelType::U8, PixelType::U16) => {
map!(U8, U16, LINEAR_U8_INTO_GAMMA22_U16);
}
(PixelType::U8x2, PixelType::U16x2) => {
map!(U8x2, U16x2, LINEAR_U8_INTO_GAMMA22_U16);
}
(PixelType::U8x3, PixelType::U16x3) => {
map!(U8x3, U16x3, LINEAR_U8_INTO_GAMMA22_U16);
}
(PixelType::U8x4, PixelType::U16x4) => {
map!(U8x4, U16x4, LINEAR_U8_INTO_GAMMA22_U16);
}
// U16 -> U8
(PixelType::U16, PixelType::U8) => {
map!(U16, U8, LINEAR_U16_INTO_GAMMA22_U8);
}
(PixelType::U16x2, PixelType::U8x2) => {
map!(U16x2, U8x2, LINEAR_U16_INTO_GAMMA22_U8);
}
(PixelType::U16x3, PixelType::U8x3) => {
map!(U16x3, U8x3, LINEAR_U16_INTO_GAMMA22_U8);
}
(PixelType::U16x4, PixelType::U8x4) => {
map!(U16x4, U8x4, LINEAR_U16_INTO_GAMMA22_U8);
}
// U16 -> U16
(PixelType::U16, PixelType::U16) => {
map!(U16, U16, LINEAR_U16_INTO_GAMMA22_U16);
}
(PixelType::U16x2, PixelType::U16x2) => {
map!(U16x2, U16x2, LINEAR_U16_INTO_GAMMA22_U16);
}
(PixelType::U16x3, PixelType::U16x3) => {
map!(U16x3, U16x3, LINEAR_U16_INTO_GAMMA22_U16);
}
(PixelType::U16x4, PixelType::U16x4) => {
map!(U16x4, U16x4, LINEAR_U16_INTO_GAMMA22_U16);
}
_ => return Err(MappingError::UnsupportedCombinationOfImageTypes),
}

Ok(())
}
71 changes: 71 additions & 0 deletions src/color/mod.rs
@@ -0,0 +1,71 @@
//! Functions for working with colorspace and gamma.
//!
//! Supported all pixel types exclude `I32` and `F32`.
//!
//! Source and destination images may have different bit depth of one pixel component.
//! But count of components must be equal.
//! For example, you may convert `U8x3` image with sRGB colorspace into
//! `U16x3` image with linear colorspace.
use crate::pixels::{GetCount, Pixel, PixelComponent, PixelComponentInto, Values};
use crate::typed_image_view::{TypedImageView, TypedImageViewMut};

pub mod gamma;
pub mod srgb;

struct MappingTable<Out: PixelComponent, const N: usize>([Out; N]);

impl<Out, const N: usize> MappingTable<Out, N>
where
Out: PixelComponent,
{
fn map<In>(&self, src_buffer: &[In], dst_buffer: &mut [Out])
where
In: PixelComponent + Into<usize>,
{
for (&src, dst) in src_buffer.iter().zip(dst_buffer) {
*dst = self.0[src.into()];
}
}

fn map_with_gaps<In>(&self, src_buffer: &[In], dst_buffer: &mut [Out], gap_step: usize)
where
In: PixelComponentInto<Out> + Into<usize>,
{
for (i, (&src, dst)) in src_buffer.iter().zip(dst_buffer).enumerate() {
if (i + 1) % gap_step != 0 {
*dst = self.0[src.into()];
} else {
*dst = src.into_component();
}
}
}

pub fn map_typed_image<S, D, CC, In>(
&self,
src_image: TypedImageView<S>,
mut dst_image: TypedImageViewMut<D>,
) where
In: PixelComponentInto<Out> + Into<usize>,
CC: GetCount,
S: Pixel<
Component = In,
ComponentsCount = CC, // Count of source pixel's components
ComponentCountOfValues = Values<N>, // Total count of values of one source pixel's component
>,
S::Component: Into<usize>,
D: Pixel<
Component = Out,
ComponentsCount = CC, // Count of destination pixel's components
>,
{
for (s_row, d_row) in src_image.iter_rows(0).zip(dst_image.iter_rows_mut()) {
let s_comp = S::components(s_row);
let d_comp = D::components_mut(d_row);
match CC::count() {
2 => self.map_with_gaps(s_comp, d_comp, 2), // Don't map alpha channel
4 => self.map_with_gaps(s_comp, d_comp, 4), // Don't map alpha channel
_ => self.map(s_comp, d_comp),
}
}
}
}

0 comments on commit c454c3b

Please sign in to comment.