Skip to content

Commit 7614ad0

Browse files
committed
Merge branch 'affine' into develop
2 parents 76c71e6 + 5c6f9cb commit 7614ad0

File tree

7 files changed

+343
-8
lines changed

7 files changed

+343
-8
lines changed

.travis.yml

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ addons:
55
apt:
66
packages:
77
- libssl-dev
8+
- gfortran
89
cache: cargo
910
rust:
1011
- stable
@@ -14,18 +15,21 @@ matrix:
1415
allow_failures:
1516
- rust: nightly
1617

17-
before_cache: |
18-
if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then
19-
RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin
20-
fi
18+
before_install:
19+
- curl https://blas-lapack-rs.github.io/travis/fortran.sh | bash
20+
21+
#before_cache: |
22+
# if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then
23+
# cargo install cargo-tarpaulin
24+
# fi
2125

2226
script:
2327
- cargo clean
2428
- cargo build
2529
- cargo test
2630

27-
after_success: |
28-
if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then
31+
# after_success: |
32+
# if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then
2933
# Uncomment the following line for coveralls.io
30-
cargo tarpaulin --ciserver travis-ci --coveralls $TRAVIS_JOB_ID
31-
fi
34+
# cargo tarpaulin --ciserver travis-ci --coveralls $TRAVIS_JOB_ID
35+
# fi

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
### Added
66
* Padding strategies (`NoPadding`, `ConstantPadding`, `ZeroPadding`)
77
* Threshold module with Otsu and Mean threshold algorithms
8+
* Image transformations and functions to create affine transform matrices
89

910
### Changed
1011
* Integrated Padding strategies into convolutions
1112
* Updated `ndarray-stats` to 0.2.0 adding `noisy_float` for median change
13+
* [INTERNAL] Disabled code coverage due to issues with tarpaulin and native libraries
1214

1315
### Removed
1416

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ edition = "2018"
1313
[dependencies]
1414
ndarray = "0.13"
1515
ndarray-stats = "0.3"
16+
ndarray-linalg = "0.12"
1617
noisy_float = "0.1"
1718
num-traits = "0.2"
1819

@@ -21,3 +22,5 @@ ndarray-rand = "0.11.0"
2122
rand = "0.7"
2223
assert_approx_eq = "1.1.0"
2324
noisy_float = "0.1.11"
25+
openblas-src = "0.7"
26+
png = "0.15"

examples/transforms.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
extern crate openblas_src;
2+
3+
use ndarray_vision::core::*;
4+
use ndarray_vision::format::netpbm::*;
5+
use ndarray_vision::format::*;
6+
use ndarray_vision::transform::affine::*;
7+
use ndarray_vision::transform::*;
8+
use std::env::current_exe;
9+
use std::f64::consts::FRAC_PI_4;
10+
use std::fs::File;
11+
use std::io::BufWriter;
12+
use std::path::{Path, PathBuf};
13+
14+
fn get_lena() -> Option<Image<u8, RGB>> {
15+
if let Ok(mut root) = current_exe() {
16+
root.pop();
17+
root.pop();
18+
root.pop();
19+
root.pop();
20+
let mut lena = PathBuf::from(&root);
21+
lena.push("images/lena.ppm");
22+
23+
let decoder = PpmDecoder::default();
24+
let image: Image<u8, _> = decoder.decode_file(lena).expect("Couldn't open Lena.ppm");
25+
Some(image)
26+
} else {
27+
None
28+
}
29+
}
30+
31+
fn main() {
32+
let lena = get_lena().expect("Couldn't load lena");
33+
34+
// Create transformation matrix
35+
let x = 0.5 * (lena.cols() as f64) - 0.5;
36+
let y = 0.5 * (lena.rows() as f64) - 0.5;
37+
let trans = rotate_around_centre(FRAC_PI_4, (x, y)).dot(&scale(0.7, 0.7));
38+
39+
let transformed = lena
40+
.transform(trans.view(), None)
41+
.expect("Transform failed");
42+
43+
// save
44+
let path = Path::new("transformed_lena.png");
45+
let file = File::create(path).expect("Couldn't create output file");
46+
let ref mut w = BufWriter::new(file);
47+
48+
let mut encoder = png::Encoder::new(w, transformed.cols() as u32, transformed.rows() as u32);
49+
encoder.set_color(png::ColorType::RGB);
50+
encoder.set_depth(png::BitDepth::Eight);
51+
52+
println!(
53+
"Writing image with resolution {}x{}",
54+
transformed.cols(),
55+
transformed.rows()
56+
);
57+
58+
let mut writer = encoder.write_header().expect("Failed to write file header");
59+
if let Some(data) = transformed.data.view().to_slice() {
60+
writer
61+
.write_image_data(data)
62+
.expect("Failed to write image data");
63+
} else {
64+
println!("Failed to get image slice");
65+
}
66+
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,5 @@ pub mod enhancement;
3333
pub mod format;
3434
/// Image processing intrinsics and common filters/algorithms.
3535
pub mod processing;
36+
/// Image transforms and warping
37+
pub mod transform;

src/transform/affine.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use ndarray::{array, prelude::*};
2+
3+
pub enum Axes {
4+
X,
5+
Y,
6+
Z,
7+
}
8+
9+
pub fn rotate_around_centre(radians: f64, centre: (f64, f64)) -> Array2<f64> {
10+
translation(centre.0, centre.1)
11+
.dot(&rotation_3d(radians, Axes::Z))
12+
.dot(&translation(-centre.0, -centre.1))
13+
}
14+
15+
pub fn rotation_2d(radians: f64) -> Array2<f64> {
16+
let s = radians.sin();
17+
let c = radians.cos();
18+
array![[c, -s], [s, c]]
19+
}
20+
21+
pub fn rotation_3d(radians: f64, ax: Axes) -> Array2<f64> {
22+
let s = radians.sin();
23+
let c = radians.cos();
24+
25+
match ax {
26+
Axes::X => array![[1.0, 0.0, 0.0], [0.0, c, -s], [0.0, s, c]],
27+
Axes::Y => array![[c, 0.0, s], [0.0, 1.0, 0.0], [-s, 0.0, c]],
28+
Axes::Z => array![[c, -s, 0.0], [s, c, 0.0], [0.0, 0.0, 1.0]],
29+
}
30+
}
31+
32+
pub fn translation(x: f64, y: f64) -> Array2<f64> {
33+
array![[1.0, 0.0, x], [0.0, 1.0, y], [0.0, 0.0, 1.0]]
34+
}
35+
36+
pub fn scale(x: f64, y: f64) -> Array2<f64> {
37+
array![[x, 0.0, 0.0], [0.0, y, 0.0], [0.0, 0.0, 1.0]]
38+
}
39+
40+
pub fn shear(x: f64, y: f64) -> Array2<f64> {
41+
array![[1.0, x, 0.0], [y, 1.0, 0.0], [0.0, 0.0, 1.0]]
42+
}

src/transform/mod.rs

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
use crate::core::{ColourModel, Image};
2+
use crate::transform::affine::translation;
3+
use ndarray::{array, prelude::*, s, Data};
4+
use ndarray_linalg::solve::Inverse;
5+
use num_traits::{Num, NumAssignOps};
6+
use std::cmp::{max, min};
7+
use std::marker::PhantomData;
8+
9+
pub mod affine;
10+
11+
#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
12+
pub enum Error {
13+
InvalidTransformation,
14+
NonInvertibleTransformation,
15+
}
16+
17+
pub trait TransformExt
18+
where
19+
Self: Sized,
20+
{
21+
/// Output type for the operation
22+
type Output;
23+
24+
/// Transforms an image given the transformation matrix and output size.
25+
/// Assume nearest-neighbour interpolation
26+
fn transform(
27+
&self,
28+
transform: ArrayView2<f64>,
29+
output_size: Option<(usize, usize)>,
30+
) -> Result<Self::Output, Error>;
31+
}
32+
33+
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
34+
struct Rect {
35+
x: isize,
36+
y: isize,
37+
w: usize,
38+
h: usize,
39+
}
40+
41+
fn source_coordinate(p: (f64, f64), trans: ArrayView2<f64>) -> (f64, f64) {
42+
let p = match trans.shape()[0] {
43+
2 => array![[p.0], [p.1]],
44+
3 => array![[p.0], [p.1], [1.0]],
45+
_ => unreachable!(),
46+
};
47+
48+
let result = trans.dot(&p);
49+
let x = result[[0, 0]];
50+
let y = result[[1, 0]];
51+
let w = match trans.shape()[0] {
52+
2 => 1.0,
53+
3 => result[[2, 0]],
54+
_ => unreachable!(),
55+
};
56+
if (w - 1.0).abs() > std::f64::EPSILON {
57+
(x / w, y / w)
58+
} else {
59+
(x, y)
60+
}
61+
}
62+
63+
fn bounding_box(dims: (f64, f64), transform: ArrayView2<f64>) -> Rect {
64+
let tl = source_coordinate((0.0, 0.0), transform);
65+
let tr = source_coordinate((0.0, dims.1), transform);
66+
let br = source_coordinate(dims, transform);
67+
let bl = source_coordinate((dims.0, 0.0), transform);
68+
69+
let tl = (tl.0.round() as isize, tl.1.round() as isize);
70+
let tr = (tr.0.round() as isize, tr.1.round() as isize);
71+
let br = (br.0.round() as isize, br.1.round() as isize);
72+
let bl = (bl.0.round() as isize, bl.1.round() as isize);
73+
74+
let leftmost = min(min(tl.0, tr.0), min(br.0, bl.0));
75+
let topmost = min(min(tl.1, tr.1), min(br.1, bl.1));
76+
let rightmost = max(max(tl.0, tr.0), max(br.0, bl.0));
77+
let bottommost = max(max(tl.1, tr.1), max(br.1, bl.1));
78+
Rect {
79+
x: leftmost,
80+
y: topmost,
81+
w: (rightmost - leftmost) as usize,
82+
h: (bottommost - topmost) as usize,
83+
}
84+
}
85+
86+
impl<T, U> TransformExt for ArrayBase<U, Ix3>
87+
where
88+
T: Copy + Clone + Num + NumAssignOps,
89+
U: Data<Elem = T>,
90+
{
91+
type Output = Array3<T>;
92+
93+
fn transform(
94+
&self,
95+
transform: ArrayView2<f64>,
96+
output_size: Option<(usize, usize)>,
97+
) -> Result<Self::Output, Error> {
98+
let shape = transform.shape();
99+
if !(shape[0] == 3 || shape[0] == 2) {
100+
Err(Error::InvalidTransformation)
101+
} else {
102+
let (mut result, new_transform) = match output_size {
103+
Some((r, c)) => (
104+
Self::Output::zeros((r, c, self.shape()[2])),
105+
transform.into_owned(),
106+
),
107+
None => {
108+
let dims = (self.shape()[0] as f64, self.shape()[1] as f64);
109+
let bounds = bounding_box(dims, transform.view());
110+
let new_trans = translation(bounds.x as f64, -bounds.y as f64).dot(&transform);
111+
(
112+
Self::Output::zeros((bounds.h, bounds.w, self.shape()[2])),
113+
new_trans,
114+
)
115+
}
116+
};
117+
118+
let transform = new_transform
119+
.inv()
120+
.map_err(|_| Error::NonInvertibleTransformation)?;
121+
for r in 0..result.shape()[0] {
122+
for c in 0..result.shape()[1] {
123+
let (x, y) = source_coordinate((c as f64, r as f64), transform.view());
124+
let x = x.round() as isize;
125+
let y = y.round() as isize;
126+
if x >= 0
127+
&& y >= 0
128+
&& (x as usize) < self.shape()[1]
129+
&& (y as usize) < self.shape()[0]
130+
{
131+
result
132+
.slice_mut(s![r, c, ..])
133+
.assign(&self.slice(s![y, x, ..]));
134+
}
135+
}
136+
}
137+
Ok(result)
138+
}
139+
}
140+
}
141+
142+
impl<T, C> TransformExt for Image<T, C>
143+
where
144+
T: Copy + Clone + Num + NumAssignOps,
145+
C: ColourModel,
146+
{
147+
type Output = Self;
148+
149+
fn transform(
150+
&self,
151+
transform: ArrayView2<f64>,
152+
output_size: Option<(usize, usize)>,
153+
) -> Result<Self::Output, Error> {
154+
let data = self.data.transform(transform, output_size)?;
155+
Ok(Self {
156+
data,
157+
model: PhantomData,
158+
})
159+
}
160+
}
161+
162+
#[cfg(test)]
163+
mod tests {
164+
extern crate openblas_src;
165+
166+
use super::affine;
167+
use super::*;
168+
use crate::core::colour_models::Gray;
169+
use std::f64::consts::PI;
170+
171+
#[test]
172+
fn translation() {
173+
let src_data = vec![2.0, 0.0, 1.0, 0.0, 5.0, 0.0, 1.0, 2.0, 3.0];
174+
let src = Image::<f64, Gray>::from_shape_data(3, 3, src_data);
175+
176+
let trans = affine::translation(2.0, 1.0);
177+
178+
let res = src.transform(trans.view(), Some((3, 3)));
179+
assert!(res.is_ok());
180+
let res = res.unwrap();
181+
182+
let expected = vec![0.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0];
183+
let expected = Image::<f64, Gray>::from_shape_data(3, 3, expected);
184+
185+
assert_eq!(expected, res)
186+
}
187+
188+
#[test]
189+
fn rotate() {
190+
let src = Image::<u8, Gray>::from_shape_data(5, 5, (0..25).collect());
191+
let trans = affine::rotate_around_centre(PI, (2.0, 2.0));
192+
let upside_down = src.transform(trans.view(), Some((5, 5))).unwrap();
193+
194+
let res = upside_down.transform(trans.view(), Some((5, 5))).unwrap();
195+
196+
assert_eq!(src, res);
197+
198+
let trans_2 = affine::rotate_around_centre(PI / 2.0, (2.0, 2.0));
199+
let trans_3 = affine::rotate_around_centre(-PI / 2.0, (2.0, 2.0));
200+
201+
let upside_down_sideways = upside_down.transform(trans_2.view(), Some((5, 5))).unwrap();
202+
let src_sideways = src.transform(trans_3.view(), Some((5, 5))).unwrap();
203+
204+
assert_eq!(upside_down_sideways, src_sideways);
205+
}
206+
207+
#[test]
208+
fn scale() {
209+
let src = Image::<u8, Gray>::from_shape_data(4, 4, (0..16).collect());
210+
let trans = affine::scale(0.5, 2.0);
211+
let res = src.transform(trans.view(), None).unwrap();
212+
213+
assert_eq!(res.rows(), 8);
214+
assert_eq!(res.cols(), 2);
215+
}
216+
}

0 commit comments

Comments
 (0)