use image::{Rgb, RgbImage};
use rayon::prelude::*;
#[inline]
fn lerp(pct: f32, a: f32, b: f32) -> f32 {
pct.mul_add(b - a, a)
}
#[inline]
fn distance(x: i32, y: i32) -> f32 {
((x * x + y * y) as f32).sqrt()
}
struct ColorCalculator {
from: [f32; 3],
to: [f32; 3],
center_x: i32,
center_y: i32,
max_dist: f32,
}
impl ColorCalculator {
fn new(from: [u8; 3], to: [u8; 3], width: u32, height: u32) -> Self {
let center_x = width as i32 / 2;
let center_y = height as i32 / 2;
Self {
from: from.map(|channel| channel as f32),
to: to.map(|channel| channel as f32),
center_x,
center_y,
max_dist: distance(center_x, center_y),
}
}
fn calculate(&self, x: u32, y: u32) -> [u8; 3] {
let x_dist = self.center_x - x as i32;
let y_dist = self.center_y - y as i32;
let t = distance(x_dist, y_dist) / self.max_dist;
[
lerp(t, self.from[0], self.to[0]) as u8,
lerp(t, self.from[1], self.to[1]) as u8,
lerp(t, self.from[2], self.to[2]) as u8,
]
}
}
fn radial_gradient(geometry: [u32; 2], inner_color: [u8; 3], outer_color: [u8; 3]) -> RgbImage {
let [width, height] = geometry;
let color_calculator = ColorCalculator::new(inner_color, outer_color, width, height);
let mut background = RgbImage::new(width, height);
(0..height / 2).into_par_iter().for_each(|y| {
for x in 0..width / 2 {
let color = Rgb(color_calculator.calculate(x, y));
background.put_pixel(x, y, color);
background.put_pixel(width - x - 1, y, color);
background.put_pixel(x, height - y - 1, color);
background.put_pixel(width - x - 1, height - y - 1, color);
};
});
background
}
I know that I could just use a mutex here although it is unnecessary since provided my code is correct no pixel should be mutated more than once. So how do I tell rust that doing background.put_pixel(x, y, color) in multiple threads is actually okay here?
I'm guessing some use of unsafe has to be used here although I am new to rust and am not sure how to use it effectively here.
Here's the error
error[E0596]: cannot borrow `background` as mutable, as it is a captured variable in a `Fn` closure
--> src\lib.rs:212:13
|
212 | background.put_pixel(x, y, color);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot borrow as mutable
error[E0596]: cannot borrow `background` as mutable, as it is a captured variable in a `Fn` closure
--> src\lib.rs:213:13
|
213 | background.put_pixel(width - x - 1, y, color);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot borrow as mutable
error[E0596]: cannot borrow `background` as mutable, as it is a captured variable in a `Fn` closure
--> src\lib.rs:214:13
|
214 | background.put_pixel(x, height - y - 1, color);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot borrow as mutable
error[E0596]: cannot borrow `background` as mutable, as it is a captured variable in a `Fn` closure
--> src\lib.rs:215:13
|
215 | background.put_pixel(width - x - 1, height - y - 1, color);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot borrow as mutable
You can't. At least not with an RgbImage.
put_pixel takes a &mut self. In Rust, it's undefined behavior to have two &mut references alias - the optimizer can do some funky stuff to your code if you break this assumption.
You will probably have an easier time creating a Vec<u8> of pixel data, calculating each pixel's value using Rayon's parallel iterators (which will take special care to not alias the mutable references), then assemble the buffer into an image using from_vec.
You can't do this with RgbImage (or any ImageBuffer), but you can do it if you work on a raw Vec<u8> in pure safe code.
Essentially the idea is to use split_at_mut and par_(r)chunks_exact_mut to produce parallel iterators that start from each corner of the image.
First, we allocate a chunk of memory
fn radial_gradient(geometry: [u32; 2], inner_color: [u8; 3], outer_color: [u8; 3]) -> RgbImage {
let [width, height] = geometry;
let color_calculator = ColorCalculator::new(inner_color, outer_color, width, height);
// assertions here to hopefully help the optimizer later
assert!(width % 2 == 0);
assert!(height % 2 == 0);
// allocate memory for the image
let mut background = RgbImage::new(width, height).into_vec();
Then, we split the memory into a top and bottom half, and create iterators over each row, starting at the top and bottom
let width = width as usize;
let height = height as usize;
// split background into top and bottom,
// so we can utilize the x-axis symmetry to reduce color calculations
let (top, bottom) = background.split_at_mut(width * height * 3 / 2);
// use chunks to split each half into rows,
// so we can utilize the y-axis symmetry to reduce color calculations
let top_rows = top.par_chunks_exact_mut(width * 3);
let bottom_rows = bottom.par_rchunks_exact_mut(width * 3);
Then, we zip those iterators together, so we iterate over the top row and bottom row together, then the next row in on each side, etc. Add enumerate to get the Y coordinate and then we will flat_map so our pixel iterator later gets unwrapped into the main iterator.
// zip to iterate over top and bottom row together
// enumerate to get the Y coordinate
top_rows.zip(bottom_rows).enumerate().flat_map(|(y, (top_row, bottom_row))| {
Then, split each row at the middle so we can have four iterators, one for each corner
// split each row at the y-axis
let (tl, tr) = top_row.split_at_mut(width * 3 / 2);
let (bl, br) = bottom_row.split_at_mut(width * 3 / 2);
// iterate over pixels (chunks of 3 bytes) from the
// top left, bottom left, top right, and bottom right half-rows
let tl = tl.par_chunks_exact_mut(3);
let bl = bl.par_chunks_exact_mut(3);
let tr = tr.par_rchunks_exact_mut(3);
let br = br.par_rchunks_exact_mut(3);
Then, zip the four pixel iterators together, so we iterate from the four corners simultaneously
// zip to iterate over each set of four pixels together
// enumerate to get the X coordinate
tl.zip_eq(bl).zip_eq(
tr.zip_eq(br)
).enumerate().map(move |(x, ((tl, bl), (tr, br)))| {
// add the y coordinate to the pixel-wise iterator
((x, y), (tl, bl, tr, br))
})
Then, iterate over each set of four pixels, copying the color into each
And convert back into an RgbImage
}).for_each(|((x, y), (tl, bl, tr, br))| {
// copy the color into the four symmetric pixels
let color = color_calculator.calculate(x as u32, y as u32);
tl.copy_from_slice(&color);
bl.copy_from_slice(&color);
tr.copy_from_slice(&color);
br.copy_from_slice(&color);
});
RgbImage::from_vec(width as u32, height as u32, background).unwrap()
}
playground
It's hard to say if this will be more or less performant than the strategy of coloring one quadrant and copying it to the rest, but it's worth a try. It may also not be worth the cognitive overhead of all of the chunking wizardry going on.
Full code
fn radial_gradient(geometry: [u32; 2], inner_color: [u8; 3], outer_color: [u8; 3]) -> RgbImage {
let [width, height] = geometry;
let color_calculator = ColorCalculator::new(inner_color, outer_color, width, height);
// assertions here to hopefully help the optimizer later
assert!(width % 2 == 0);
assert!(height % 2 == 0);
// allocate memory for the image
let mut background = RgbImage::new(width, height).into_vec();
let width = width as usize;
let height = height as usize;
// split background into top and bottom,
// so we can utilize the x-axis symmetry to reduce color calculations
let (top, bottom) = background.split_at_mut(width * height * 3 / 2);
// use chunks to split each half into rows,
// so we can utilize the y-axis symmetry to reduce color calculations
let top_rows = top.par_chunks_exact_mut(width * 3);
let bottom_rows = bottom.par_rchunks_exact_mut(width * 3);
// zip to iterate over top and bottom row together
// enumerate to get the Y coordinate
top_rows.zip(bottom_rows).enumerate().flat_map(|(y, (top_row, bottom_row))| {
// split each row at the y-axis
let (tl, tr) = top_row.split_at_mut(width * 3 / 2);
let (bl, br) = bottom_row.split_at_mut(width * 3 / 2);
// iterate over pixels (chunks of 3 bytes) from the
// top left, bottom left, top right, and bottom right half-rows
let tl = tl.par_chunks_exact_mut(3);
let bl = bl.par_chunks_exact_mut(3);
let tr = tr.par_rchunks_exact_mut(3);
let br = br.par_rchunks_exact_mut(3);
// zip to iterate over each set of four pixels together
// enumerate to get the X coordinate
tl.zip_eq(bl).zip_eq(
tr.zip_eq(br)
).enumerate().map(move |(x, ((tl, bl), (tr, br)))| {
// add the y coordinate to the pixel-wise iterator
((x, y), (tl, bl, tr, br))
})
}).for_each(|((x, y), (tl, bl, tr, br))| {
// copy the color into the four symmetric pixels
let color = color_calculator.calculate(x as u32, y as u32);
tl.copy_from_slice(&color);
bl.copy_from_slice(&color);
tr.copy_from_slice(&color);
br.copy_from_slice(&color);
});
RgbImage::from_vec(width as u32, height as u32, background).unwrap()
}
Related
I'm learning Rust and wanted to try my hand at error diffusion dithering. I've got it working, but the dithered file ends up bigger than the original, which is the opposite of what's supposed to happen. The original JPEG image is 605 KB big, but the dithered image has a whopping 2.57 MB. My knowledge of the image crate is very limited and I found all the various structs for representing images confusing, so I must be missing something regarding the API.
Here's the code for dithering the image (included are only parts which I deemed relevant):
impl DiffusionKernel<'_> {
pub const FLOYD_STEINBERG: DiffusionKernel<'_> = // Constructor
fn distribute_error(
&self,
error: &(i16, i16, i16),
image: &mut DynamicImage,
width: u32,
height: u32,
x: u32,
y: u32,
) {
for target in self.targets /* "targets" are the pixels where to distribute the error */ {
// Checks if the target x and y are in the bounds of the image
// Also returns the x and y coordinates of the pixel, because the "target" struct only describes the offset of the target pixel from the pixel being currently processed
let (is_valid_target, target_x, target_y) =
DiffusionKernel::is_valid_target(target, width, height, x, y);
if is_valid_target == false {
continue;
}
let target_pix = image.get_pixel(target_x, target_y);
// Distribute the error to the target_pix
let new_pix = Rgba::from([new_r, new_g, new_b, 255]);
image.put_pixel(target_x, target_y, new_pix);
}
}
pub fn diffuse(&self, bit_depth: u8, image: &mut DynamicImage) {
let width = image.width();
let height = image.height();
for x in 0..width {
for y in 0..height {
let pix = image.get_pixel(x, y);
let pix_quantized = ColorUtil::reduce_color_bit_depth(pix, bit_depth); // Quantizes the color
let error = (
pix.0[0] as i16 - pix_quantized.0[0] as i16,
pix.0[1] as i16 - pix_quantized.0[1] as i16,
pix.0[2] as i16 - pix_quantized.0[2] as i16,
);
image.put_pixel(x, y, pix_quantized);
self.distribute_error(&error, image, width, height, x, y);
}
}
// Distributing the error ends up creating colors like 7, 7, 7, or 12, 12, 12 instead of 0, 0, 0 for black,
// so here I'm just ensuring that the colors are correctly quantized.
// I think the algorithm shouldn't behave like this, I'll try to fix it later.
for x in 0..width {
for y in 0..height {
let pix = image.get_pixel(x, y);
let pix_quantized = ColorUtil::reduce_color_bit_depth(pix, bit_depth);
image.put_pixel(x, y, pix_quantized);
}
}
}
}
Here's the code for loading and saving the image:
let format = "jpg";
let path = String::from("C:\\...\\Cat.".to_owned() + format);
let trimmed_path = path.trim(); // This needs to be here if I'm getting the path from the console
let bfr = Reader::open(trimmed_path)
.unwrap()
.with_guessed_format()
.unwrap();
let mut dynamic = bfr.decode().unwrap();
// dynamic = dynamic.grayscale();
error_diffusion::DiffusionKernel::FLOYD_STEINBERG.diffuse(1, &mut dynamic);
dynamic
.save(trimmed_path.to_owned() + "_dithered." + format)
.expect("There was an error saving the image.");
Ok, so I got back to trying to figure this out, and it looks like you just need to use an image encoder like the PngEncoder and a file to write to in order to lower the bit depth of an image. The encoder takes bytes, not pixels, but thankfully, images have a as_bytes method which returns what you need.
Here's the code:
let img = image::open(path).expect("Failed to open image.");
let (width, height) = img.dimensions();
let writer = File::create(path.to_owned() + "_out.png").unwrap();
// This is the best encoder configuration for black/white images, which is my output
// Grayscale with multiple colors -> black/white using dithering
let encoder = PngEncoder::new_with_quality(writer, CompressionType::Best, FilterType::NoFilter);
encoder
.write_image(img.as_bytes(), width, height, ColorType::L8)
.expect("Failed to write image.");
use rayon::prelude::*;
use image::RgbImage;
#[inline]
fn lerp(pct: f32, a: f32, b: f32) -> f32 {
pct.mul_add(b - a, a)
}
#[inline]
fn distance(x: i32, y: i32) -> f32 {
((x * x + y * y) as f32).sqrt()
}
struct ColorCalculator {
from: [f32; 3],
to: [f32; 3],
center_x: i32,
center_y: i32,
max_dist: f32,
}
impl ColorCalculator {
fn new(from: [u8; 3], to: [u8; 3], width: u32, height: u32) -> Self {
let center_x = width as i32 / 2;
let center_y = height as i32 / 2;
Self {
from: from.map(|channel| channel as f32),
to: to.map(|channel| channel as f32),
center_x,
center_y,
max_dist: distance(center_x, center_y),
}
}
fn calculate(&self, x: u32, y: u32) -> [u8; 3] {
let x_dist = self.center_x - x as i32;
let y_dist = self.center_y - y as i32;
let t = distance(x_dist, y_dist) / self.max_dist;
}
}
pub fn radial_gradient_mirror(
geometry: [u32; 2],
inner_color: [u8; 3],
outer_color: [u8; 3],
) -> RgbImage {
let [width, height] = geometry;
let color_calculator = ColorCalculator::new(inner_color, outer_color, geometry[0], geometry[1]);
let mut buf: Vec<_> = (0..(height / 2))
.into_par_iter()
.flat_map(|y| {
let mut row: Vec<[u8; 3]> = Vec::with_capacity(width as usize);
for x in 0..(width / 2) {
row.push(color_calculator.calculate(x, y))
}
row.extend(row.clone().iter().rev());
row
})
.collect();
buf.extend(buf.clone().iter().rev());
let buf = buf.into_iter().flatten().collect();
RgbImage::from_raw(width, height, buf).unwrap()
}
I have an algorithm to generate a radial gradient. The original version would calculate the color of each pixel but because there is a horizontal and vertical line of symmetry I can calculate the colors for the top left corner and use some vector manipulation to mirror. I managed to do this by cloning and reversing the iterator buf.clone().iter().rev() although this is slow. How can I avoid this and are there any other optimizations I could use?
You do not really need to populate the buffer as later you override it, you can use direct chained iterators for doing so (copied is used to to have owned values), original buff will be droped at the end of the scope:
let i1 = buf.iter().copied();
let i2 = buf.iter().copied().rev();
let buf = i1.chain(i2).flatten().collect();
Full method:
pub fn radial_gradient_mirror(
geometry: [u32; 2],
inner_color: [u8; 3],
outer_color: [u8; 3],
) -> RgbImage {
let [width, height] = geometry;
let color_calculator = ColorCalculator::new(inner_color, outer_color, geometry[0], geometry[1]);
let mut buf: Vec<_> = (0..(height / 2))
.into_par_iter()
.flat_map(|y| {
let mut row: Vec<[u8; 3]> = Vec::with_capacity(width as usize);
for x in 0..(width / 2) {
row.push(color_calculator.calculate(x, y))
}
row.extend(row.clone().iter().rev());
row
})
.collect();
let i1 = buf.iter().copied();
let i2 = buf.iter().copied().rev();
let buf = i1.chain(i2).flatten().collect();
RgbImage::from_raw(width, height, buf).unwrap()
}
Playground
I believe your core question is how to optimize this:
buf.extend(buf.clone().iter().rev());
Vec has extend_from_within which can be used to extend a Vec from its own contents... however you want the results in reverse order. So I don't think there is a neat way using iterators to express this, since we're mutating and iterating over the same Vec at the same time. However doing this using a loop isn't too bad:
fn mirror_vec<T: Clone>(v: &mut Vec<T>) {
let n = v.len();
v.reserve(n);
for i in (0..n).rev() {
v.push(v[i].clone());
}
}
Note that the clone is not the only performance bottleneck here, and probably not the main one either. A very big issue is using a Vec<Vec<_>> for a 2D array. It is much faster to use a single flat array and some indexing magic:
pub fn radial_gradient_mirror(
geometry: [u32; 2],
inner_color: [u8; 3],
outer_color: [u8; 3],
) -> RgbImage {
let width = geometry[0] as usize;
let height = geometry[1] as usize;
let color_calculator = ColorCalculator::new(inner_color, outer_color, geometry[0], geometry[1]);
// Note: need to check that dimensions are even, or handle the case when they are odd! */
let mut buf = Vec::with_capacity (width * height * 3);
for r in 0..height/2 {
for c in 0..width/2 {
buf.extend_from_slice (&color_calculator.calculate (c, r));
}
for c in width/2+1 .. width {
buf.extend_from_within ((r*width + width-c-1)*3 .. (r*width + width-c)*3);
}
}
for r in height/2+1 .. height {
buf.extend_from_within ((height-r-1) * width * 3 .. (height-r) * width * 3);
}
RgbImage::from_raw(width as u32, height as u32, buf).unwrap()
}
Playground
Note that as with all performance-related questions, you should benchmark in release mode to make sure that speed follows your expectations. In particular on modern computers, memory accesses are much slower than the CPU and it is often faster to re-compute a value rather than try to get it from a cached memory location!
I have ndarray
let mut a = Array3::<u8>::from_elem((50, 40, 3), 3);
and I use image library
let mut imgbuf = image::ImageBuffer::new(50, 40);
How could I save my ndarray as image ?
If there is better image library then image for this I could use it.
The easiest way is to ensure that the array follows is in standard layout (C-contiguous) with the image dimensions in the order (height, width, channel) order (HWC), or in an equivalent memory layout. This is necessary because image expects rows to be contiguous in memory.
Then, build a RgbImage using the type's from_raw function.
use image::RgbImage;
use ndarray::Array3;
fn array_to_image(arr: Array3<u8>) -> RgbImage {
assert!(arr.is_standard_layout());
let (height, width, _) = arr.dim();
let raw = arr.into_raw_vec();
RgbImage::from_raw(width as u32, height as u32, raw)
.expect("container should have the right size for the image dimensions")
}
Example of use:
let mut array: Array3<u8> = Array3::zeros((200, 250, 3)); // 250x200 RGB
for ((x, y, z), v) in array.indexed_iter_mut() {
*v = match z {
0 => y as u8,
1 => x as u8,
2 => 0,
_ => unreachable!(),
};
}
let image = array_to_image(array);
image.save("out.png")?;
The output image:
Below are a few related helper functions, in case they are necessary.
Ndarrays can be converted to standard layout by calling the method as_standard_layout, available since version 0.13.0. Before this version, you would need to collect each array element into a vector and rebuild the array, like so:
fn to_standard_layout<A, D>(arr: Array<A, D>) -> Array<A, D>
where
A: Clone,
D: Dimension,
{
let v: Vec<_> = arr.iter().cloned().collect();
let dim = arr.dim();
Array::from_shape_vec(dim, v).unwrap()
}
Moreover, converting an ndarray in the layout (width, height, channel) to (height, width, channel) is also possible by swapping the first two axes and making the array C-contiguous afterwards:
fn wh_to_hw(mut arr: Array3<u8>) -> Array3<u8> {
arr.swap_axes(0, 1);
arr.as_standard_layout().to_owned()
}
I want to implement line detection in a simple coordinate system. I roughly followed an article about how to implement the Hough Transform, but the results I get are quite far off from what I want.
Given a 3 x 3 matrix like this:
X X X
X X X
- - -
I want to detect the line starting at 0,0 going to 2,0. I represent the coordinate system as a simple array of tuples, the first item in the tuple is x, the second is y, the third is the type of the point (canvas or line).
I thought it would be relatively easy to detect the line using Hough, because the edge detection is basically just a binary decision: either the tuple is of type line, or not.
I implemented the following program in Rust:
use std::f32;
extern crate nalgebra as na;
use na::DMatrix;
#[derive(Debug, PartialEq, Clone)]
enum Representation {
Canvas,
Line,
}
fn main () {
let image_width = 3;
let image_height = 3;
let grid = vec![
(0, 0, Representation::Line), (1, 0, Representation::Line), (2, 0, Representation::Line),
(0, 1, Representation::Canvas), (1, 1, Representation::Canvas), (2, 1, Representation::Canvas),
(0, 2, Representation::Canvas), (1, 2, Representation::Canvas), (2, 2, Representation::Canvas),
];
//let tmp:f32 = (image_width as f32 * image_width as f32) + (image_height as f32 * image_height as f32);
let max_line_length = 3;
let mut accumulator = DMatrix::from_element(180, max_line_length as usize, 0);
for y in 0..image_height {
for x in 0..image_width {
let coords_index = (y * image_width) + x;
let coords = grid.get(coords_index as usize).unwrap();
// check if coords is an edge
if coords.2 == Representation::Line {
for angle in 0..180 {
let r = (x as f32) * (angle as f32).cos() + (y as f32) * (angle as f32).sin();
let r_scaled = scale_between(r, 0.0, 2.0, -2.0, 2.0).round() as u32;
accumulator[(angle as usize, r_scaled as usize)] += 1;
}
}
}
}
let threshold = 3;
// z = angle
for z in 0..180 {
for r in 0..3 {
let val = accumulator[(z as usize, r as usize)];
if val < threshold {
continue;
}
let px = (r as f32) * (z as f32).cos();
let py = (r as f32) * (z as f32).sin();
let p1_px = px + (max_line_length as f32) * (z as f32).cos();
let p1_py = py + (max_line_length as f32) * (z as f32).sin();
let p2_px = px - (max_line_length as f32) * (z as f32).cos();
let p2_py = px - (max_line_length as f32) * (z as f32).cos();
println!("Found lines from {}/{} to {}/{}", p1_px.ceil(), p1_py.ceil(), p2_px.ceil(), p2_py.ceil());
}
}
}
fn scale_between(unscaled_num: f32, min_allowed: f32, max_allowed: f32, min: f32, max: f32) -> f32 {
(max_allowed - min_allowed) * (unscaled_num - min) / (max - min) + min_allowed
}
The result is something like:
Found lines from -1/4 to 1/1
Found lines from 2/4 to 0/0
Found lines from 2/-3 to 0/0
Found lines from -1/4 to 1/1
Found lines from 1/-3 to 0/0
Found lines from 0/4 to 1/1
...
Which is actually quite a lot, given that I only want to detect a single line. My implementation clearly is wrong, but I don't know where to look, my maths-fu is not high enough to debug further.
I think the first part, the actual Hough Transform, seems kind of correct, because the linked article says:
for each image point p
{
if (p is part of an edge)
{
for each possible angle
{
r = x * cos(angle) + y * sin(angle);
houghMatrix[angle][r]++;
}
}
}
I'm stuck at mapping and filtering, which is according to the article:
Each point in Hough space is given by angle a and distance r. Using these values, one single point p(x,y) of the line can be calculated by
px = r * cos(angle)
py = r * sin(angle).
The maximum length of a line is restricted by sqrt(imagewidth2 + imageheight2).
The point p, the angle a of the line and the maximum line length 'maxLength' can be used to calculate two other points of the line. The maximum length here ensures that both points to be calculated are lying outside of the actual image, resulting in the fact that if a line is drawn between these two points, the line goes from image border to image border in any case and is never cropped somewhere inside the image.
These two points p1 and p2 are calculated by:
p1_x = px + maxLength * cos(angle);
p1_y = py + maxLength * sin(angle);
p2_x = px - maxLength * cos(angle);
p2_y = py - maxLength * sin(angle);
...
EDIT
Updated version that takes the image size into account, as suggested by #RaymoAisla
use std::f32;
extern crate nalgebra as na;
use na::DMatrix;
fn main () {
let image_width = 3;
let image_height = 3;
let mut grid = DMatrix::from_element(image_width as usize, image_height as usize, 0);
grid[(0, 0)] = 1;
grid[(1, 0)] = 1;
grid[(2, 0)] = 1;
let accu_width = 7;
let accu_height = 3;
let max_line_length = 3;
let mut accumulator = DMatrix::from_element(accu_width as usize, accu_height as usize, 0);
for y in 0..image_height {
for x in 0..image_width {
let coords = (x, y);
let is_edge = grid[coords] == 1;
if !is_edge {
continue;
}
for i in 0..7 {
let angle = i * 30;
let r = (x as f32) * (angle as f32).cos() + (y as f32) * (angle as f32).sin();
let r_scaled = scale_between(r, 0.0, 2.0, -2.0, 2.0).round() as u32;
accumulator[(i as usize, r_scaled as usize)] += 1;
println!("angle: {}, r: {}, r_scaled: {}", angle, r, r_scaled);
}
}
}
let threshold = 3;
// z = angle index
for z in 0..7 {
for r in 0..3 {
let val = accumulator[(z as usize, r as usize)];
if val < threshold {
continue;
}
let px = (r as f32) * (z as f32).cos();
let py = (r as f32) * (z as f32).sin();
let p1_px = px + (max_line_length as f32) * (z as f32).cos();
let p1_py = py + (max_line_length as f32) * (z as f32).sin();
let p2_px = px - (max_line_length as f32) * (z as f32).cos();
let p2_py = px - (max_line_length as f32) * (z as f32).cos();
println!("Found lines from {}/{} to {}/{} - val: {}", p1_px.ceil(), p1_py.ceil(), p2_px.ceil(), p2_py.ceil(), val);
}
}
}
fn scale_between(unscaled_num: f32, min_allowed: f32, max_allowed: f32, min: f32, max: f32) -> f32 {
(max_allowed - min_allowed) * (unscaled_num - min) / (max - min) + min_allowed
}
The reported output is now:
angle: 0, r: 0, r_scaled: 1
angle: 30, r: 0, r_scaled: 1
angle: 60, r: 0, r_scaled: 1
angle: 90, r: 0, r_scaled: 1
angle: 120, r: 0, r_scaled: 1
angle: 150, r: 0, r_scaled: 1
angle: 180, r: 0, r_scaled: 1
...
Found lines from 3/4 to -1/-1
Found lines from -3/1 to 2/2
I plotted the lines on a coordinate system, the lines are very far off from the line that I would expect. I wonder if the conversion back to points is still off.
Your angles are in degrees rather than radians!
Rust, like all other programming languages, uses radians for its trigonometry functions. Running
let ang_d = 30.0;
let ang_r = ang_d * 3.1415926 / 180.0;
println!("sin(30) {} sin(30*pi/180) {}", (ang_d as f32).sin(), (ang_r as f32).sin());
gives the results
sin(30) -0.9880316 sin(30*pi/180) 0.5
You need to convert all your angles to radians before calling cos and sin.
In the first loop I've got
let angle = (i as f32) * 30.0 * 3.1415926 / 180.0;
let r = (x as f32) * (angle as f32).cos() + (y as f32) * (angle as f32).sin();
and in the second where you calculate the points on the lines
let ang = (z as f32) * 30.0 * 3.1415926 / 180.0;
let px = (r as f32) * (ang as f32).cos();
let py = (r as f32) * (ang as f32).sin();
let p1_px = px + (max_line_length as f32) * (ang as f32).cos();
let p1_py = py + (max_line_length as f32) * (ang as f32).sin();
let p2_px = px - (max_line_length as f32) * (ang as f32).cos();
let p2_py = px - (max_line_length as f32) * (ang as f32).cos();
My Rust is rusty (actually non-existent) so there is nicer ways of doing the conversion and there should be a constant with the exact value of pi somewhere.
Hough Transform principle is to search all the lines passing through each considered point, and counting these lines occurrences thanks to the accumulator.
However, we cannot determine all these lines because their number is infinite. Moreover the image is discretized, so calculating all lines does not make sense.
And the problem comes from this discretization. The angle discretization needs to be relevant with the image size. Here, calculating the radius for 180 angles is overcalculation, because the image only make 9 pixels, and the possible angles for any line in this image are restricted to a dozen value.
So here, for the first point (0,0), for each angle, the associated radius is r = 0
For the second (1,0), the associated radius is r = cos(angle)
For the third (2,0), the associated radius is r = 2 cos(angle)
With the rounding, numerous values will have an associated radius of 0 for the same angle, and it cause over-detection. Discretization causes a spreading of Hough Accumulator.
So the radius and the angle discretization needs to be calculated depending on the image size. Here, a 30° step, so a 7*3 accumulator will be sufficient to detect a line.
I'm building a simple 2D game in Rust using Piston. I used examples from the Piston documentation and expanded it and it works quite well. However, I get pretty bad performance:
Drawing only 2 squares gives me a framerate of about 30-40 FPS
Drawing 5 000 squares gives me a framerate of about 5 FPS
This is on a Core i7 # 2.2GHz running Windows 10. Rust version 1.8, Piston version 0.19.0.
Is this expected or have I made any mistakes in my code? Am I even measuring the FPS correctly?
extern crate piston_window;
extern crate piston;
extern crate rand;
use piston_window::*;
use rand::Rng;
fn main() {
const SIZE: [u32; 2] = [600,600];
const GREEN: [f32; 4] = [0.0, 1.0, 0.0, 1.0];
const NUM: u32 = 1000; //change this to change number of polygons
const SQUARESIZE: f64 = 10.0;
// Create an Glutin window.
let window: PistonWindow = WindowSettings::new("test",SIZE)
.exit_on_esc(true)
.build()
.unwrap();
let mut frames = 0;
let mut passed = 0.0;
let mut rng = rand::thread_rng();
for e in window {
if let Some(_) = e.render_args() {
e.draw_2d(|c, g| {
//clear the screen.
clear(GREEN, g);
for i in 0..NUM {
//setting up so that it looks pretty
let x = (i % SIZE[0]) as f64;
let y = (i % SIZE[1]) as f64;
let fill = (x / (SIZE[0] as f64)) as f32;
let color: [f32; 4] = [fill,1.0-fill,fill,fill];
let x = rng.gen_range::<f64>(0.0,SIZE[0] as f64);
//draw the square
let square = rectangle::square(0.0, 0.0, SQUARESIZE);
let transform = c.transform.trans(x-SQUARESIZE/2.0,y-SQUARESIZE/2.0);
rectangle(color, square, transform, g);
}
frames+=1;
});
}
if let Some(u) = e.update_args() {
passed += u.dt;
if passed > 1.0 {
let fps = (frames as f64) / passed;
println!("FPS: {}",fps);
frames = 0;
passed = 0.0;
}
}
}
}
Thank you for your help.
EDIT: taskmgr tells me that it only uses about 17K memory, but one of my physical CPU cores maxes out when the FPS drops below about 20.
EDIT 2: Changed the code to a complete working example.