Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image Normalization #160

Merged
merged 9 commits into from
May 29, 2021
8 changes: 8 additions & 0 deletions Cargo.lock

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

9 changes: 9 additions & 0 deletions proc_blocks/image-normalization/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "image-normalization"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
runic-types = { path = "../../runic-types" }
177 changes: 177 additions & 0 deletions proc_blocks/image-normalization/src/distribution.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
use core::{
convert::{TryFrom, TryInto},
fmt::{self, Display, Formatter},
};

/// A normal distribution.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Distribution {
pub mean: f32,
pub standard_deviation: f32,
}

impl Distribution {
pub fn new(mean: f32, standard_deviation: f32) -> Self {
assert_ne!(
standard_deviation, 0.0,
"The standard deviation must be non-zero"
);

Distribution {
mean,
standard_deviation,
}
}

pub fn z_score(&self, value: f32) -> f32 {
(value - self.mean) / self.standard_deviation
}
}

impl Default for Distribution {
fn default() -> Self {
Distribution {
mean: 0.0,
standard_deviation: 1.0,
}
}
}

impl From<[f32; 2]> for Distribution {
fn from([mean, standard_deviation]: [f32; 2]) -> Self {
Distribution {
mean,
standard_deviation,
}
}
}

impl From<[i32; 2]> for Distribution {
fn from([mean, standard_deviation]: [i32; 2]) -> Self {
Distribution::new(mean as f32, standard_deviation as f32)
}
}

impl<'a> TryFrom<[&'a str; 2]> for Distribution {
type Error = DistributionConversionError<'a>;

fn try_from([mean, std_dev]: [&'a str; 2]) -> Result<Self, Self::Error> {
let mean: f32 = mean.parse().map_err(|err| {
DistributionConversionError::ParseFloat { value: mean, err }
})?;
let standard_deviation: f32 = std_dev.parse().map_err(|err| {
DistributionConversionError::ParseFloat {
value: std_dev,
err,
}
})?;

Ok(Distribution {
mean,
standard_deviation,
})
}
}

impl<'a, T> TryFrom<&'a [T; 2]> for Distribution
where
Distribution: TryFrom<[T; 2]>,
T: Copy,
{
type Error = <Distribution as TryFrom<[T; 2]>>::Error;

fn try_from(input: &'a [T; 2]) -> Result<Self, Self::Error> {
Distribution::try_from(*input)
}
}

impl<'a, T> TryFrom<&'a [T]> for Distribution
where
Distribution: TryFrom<[T; 2], Error = DistributionConversionError<'a>>,
T: Copy,
{
type Error = DistributionConversionError<'a>;

fn try_from(input: &'a [T]) -> Result<Self, Self::Error> {
match *input {
[mean, std_dev] => [mean, std_dev].try_into(),
_ => Err(DistributionConversionError::IncorrectArrayLength {
actual_length: input.len(),
}),
}
}
}

impl<T> TryFrom<(T, T)> for Distribution
where
Distribution: TryFrom<[T; 2]>,
{
type Error = <Distribution as TryFrom<[T; 2]>>::Error;

fn try_from((mean, std_dev): (T, T)) -> Result<Self, Self::Error> {
[mean, std_dev].try_into()
}
}

#[derive(Debug, Clone, PartialEq)]
pub enum DistributionConversionError<'a> {
ParseFloat {
value: &'a str,
err: core::num::ParseFloatError,
},
IncorrectArrayLength {
actual_length: usize,
},
}

impl<'a> Display for DistributionConversionError<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
DistributionConversionError::ParseFloat { value, err } => {
write!(f, "Unable to parse \"{}\" as a number: {}", value, err)
},
DistributionConversionError::IncorrectArrayLength {
actual_length,
} => write!(
f,
"Expected an array with 2 elements but found {}",
actual_length
),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn calculate_z_score() {
let distribution = Distribution::new(100.0, 15.0);
let value = 110.0;

let got = distribution.z_score(value);

assert_eq!(got, 0.6666667);
}

#[test]
fn parse_distribution_from_two_strings() {
let src = &["1.75", "5"];
let should_be = Distribution::new(1.75, 5.0);

let got: Distribution = src.try_into().unwrap();

assert_eq!(got, should_be);
}

#[test]
fn incorrect_slice_length() {
let src = &["1.75", "5"];
let should_be = Distribution::new(1.75, 5.0);

let got: Distribution = src.try_into().unwrap();

assert_eq!(got, should_be);
}
}
149 changes: 149 additions & 0 deletions proc_blocks/image-normalization/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#![no_std]

mod distribution;

pub use crate::distribution::{Distribution, DistributionConversionError};

use core::{convert::TryInto, fmt::Display};
use runic_types::{HasOutputs, Tensor, Transform, TensorViewMut};

#[derive(Debug, Default, Clone, PartialEq)]
#[non_exhaustive]
pub struct ImageNormalization {
pub red: Distribution,
pub green: Distribution,
pub blue: Distribution,
}

impl ImageNormalization {
/// A shortcut for initializing the red, green, and blue distributions in
/// one call.
pub fn set_rgb<D>(&mut self, distribution: D) -> &mut Self
where
D: TryInto<Distribution>,
D::Error: Display,
{
let d = match distribution.try_into() {
Ok(d) => d,
Err(e) => panic!("Invalid distribution: {}", e),
};

self.set_red(d).set_green(d).set_blue(d)
}

pub fn set_red<D>(&mut self, distribution: D) -> &mut Self
where
D: TryInto<Distribution>,
D::Error: Display,
{
match distribution.try_into() {
Ok(d) => self.red = d,
Err(e) => panic!("Invalid distribution: {}", e),
}

self
}

pub fn set_green<D>(&mut self, distribution: D) -> &mut Self
where
D: TryInto<Distribution>,
D::Error: Display,
{
match distribution.try_into() {
Ok(d) => self.green = d,
Err(e) => panic!("Invalid distribution: {}", e),
}

self
}

pub fn set_blue<D>(&mut self, distribution: D) -> &mut Self
where
D: TryInto<Distribution>,
D::Error: Display,
{
match distribution.try_into() {
Ok(d) => self.blue = d,
Err(e) => panic!("Invalid distribution: {}", e),
}

self
}
}

impl Transform<Tensor<u8>> for ImageNormalization {
type Output = Tensor<f32>;

fn transform(&mut self, input: Tensor<u8>) -> Self::Output {
self.transform(input.map(|_dims, &elem| elem as f32))
}
}

impl Transform<Tensor<f32>> for ImageNormalization {
type Output = Tensor<f32>;

fn transform(&mut self, mut input: Tensor<f32>) -> Self::Output {
let mut view = input.view_mut::<3>()
.expect("The image normalization proc block only supports outputs of the form [channels, rows, columns]");

let [channels, _, _] = view.dimensions();

assert_eq!(
channels, 3,
"The image must have 3 channels - red, green, and blue"
);

transform(self.red, 0, &mut view);
transform(self.green, 1, &mut view);
transform(self.blue, 2, &mut view);

input
}
}

fn transform(
d: Distribution,
channel: usize,
view: &mut TensorViewMut<'_, f32, 3>,
) {
let [_, rows, columns] = view.dimensions();

for row in 0..rows {
for column in 0..columns {
let ix = [channel, row, column];
let current_value = view[ix];
view[ix] = d.z_score(current_value);
}
}
}

impl HasOutputs for ImageNormalization {
fn set_output_dimensions(&mut self, dimensions: &[usize]) {
match *dimensions {
[1, _, _] | [3, _, _] => {},
[channels, _, _] => panic!(
"The number of channels should be either 1 or 3, found {}",
channels
),
_ => panic!("The image normalization proc block only supports outputs of the form [channels, rows, columns], found {:?}", dimensions),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn normalizing_with_default_distribution_is_noop() {
let red = [[1.0], [4.0], [7.0], [10.0]];
let green = [[2.0], [5.0], [8.0], [11.0]];
let blue = [[3.0], [6.0], [9.0], [12.0]];
let image: Tensor<f32> = Tensor::from([red, green, blue]);
let mut norm = ImageNormalization::default();

let got = norm.transform(image.clone());

assert_eq!(got, image);
}
}
1 change: 1 addition & 0 deletions python/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ doctest = false

[dependencies]
fft = { path = "../proc_blocks/fft" }
image-normalization = { path = "../proc_blocks/image-normalization" }
noise-filtering = { path = "../proc_blocks/noise-filtering" }
normalize = { path = "../proc_blocks/normalize" }
numpy = "0.13.1"
Expand Down
Loading