How to make a circle image out of radius?

Im working on a rust function that takes the radius,the color and a boolean stating where the border should be degradated to create a circle image.

The problem is that the code generates a diagonal directioned square instead of a circle and when the last boolean is true. Almost all is difuminated except a line from top to bottom.

here's the code with the samples of images https://github.com/IronThread/image_utils2

Any contribution will be welcome and thanks for your attention.

are you translating some code from other languages? I didn't bother to read it as it is so hard to read. but from the output images, I'm pretty sure you are comparing the "manhattan distance" with the radius, but you are supposed to calculate the "euclidean distance" instead.

I'd suggest use a drawing library, e.g. to draw a filled circle shape usig plotters:

	use plotters::prelude::*;
	let canvas = BitMapBackend::new("circle.png", (512, 512)).into_drawing_area();
	canvas.fill(&TRANSPARENT)?;
	let shape = Circle::new((100, 100), 50, ShapeStyle::from(RED).filled());
	canvas.draw(&shape).unwrap();
	canvas.present().unwrap()
2 Likes

I also can't really understand the code, but it looks like your vec pixel buffer is too small. It can only hold approx one byte value per pixel, whereas you need four bytes per pixel (one for each of the RGBA channels).

If you are not going to use a third-party crate, then drawing a circle shouldn't be that complicated. The circle function would look something like (code below not tested):

pub fn circle(radius: usize, color: [u8; 3]) -> Option<RgbaImage> {

    let vec_capacity = 16 * radius.pow(2);
    let mut vec: Vec<u8> = vec![0; vec_capacity];

    let center_point = (radius, radius);

    for index in 0..vec_capacity {
        // (0, 0) at top-left, right is +ve x, down is +ve y
        // each pixel takes up 4 bytes, one for each of the RGBA channels
        let cur_x_y = ((index / 4) % (radius * 2), (index / 4) / (radius * 2));

        let sq_distance = (cur_x_y.0 as i32 - center_point.0 as i32).pow(2)
            + (cur_x_y.1 as i32 - center_point.1 as i32).pow(2);

        if sq_distance < radius.pow(2) as i32 {
            vec[index] = match index % 4 {
                0..2 => color[index % 4],
                _ => 255,                // fixed alpha value
            }
        }
    }
    let a = 2 * radius as u32;
    RgbaImage::from_vec(a, a, vec)
}
1 Like

just to want to add, the reason I'm guessing you are translating code from other languages, is the non idiomatic way how you use iterator to iterate through columns, yet you use an outer for loop iterate through the rows. mis-use iterators is a common mistake for new rustaceans.

just for your reference, there's an enumerate_pixels() iterator for Image, you can use it like so:

// totally unnecessary, see `lerp_rgba()`
#![feature(array_zip)]

use image::{Rgba, RgbaImage};

fn dist_sqr(x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
	(x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)
}

fn red_circle(cx: f32, cy: f32, r: f32) -> RgbaImage {
	let mut image = RgbaImage::new(1 + 2 * cx as u32, 1 + 2 * cy as u32);
	let r_sqr = r * r;
	for (x, y, pixel) in image.enumerate_pixels_mut() {
		let dist_sqr = dist_sqr(x as f32, y as f32, cx, cy);
		if dist_sqr <= r_sqr {
			*pixel = Rgba([255, 0, 0, 255]);
		} else {
			*pixel = Rgba([0; 4]);
		}
	}
	image
}

if you want a gradient instead of a solid color, just do an interpolation (with an easing function if you want be fancy)

fn lerp(t: f32, a: f32, b: f32) -> f32 {
	(1.0 - t) * a + t * b
}

fn lerp_rgba(t: f32, a: Rgba<u8>, b: Rgba<u8>) -> Rgba<u8> {
	// `array_zip` need nightly toolchain, you can manually unroll the loop anyway
	Rgba(a.0.zip(b.0).map(|(a, b)| lerp(t, a as f32, b as f32) as u8))
}

fn fancy_circle(cx: f32, cy: f32, r: f32, easing: impl Fn(f32) -> f32) -> RgbaImage {
	let mut image = RgbaImage::new(1 + 2 * cx as u32, 1 + 2 * cy as u32);
	let r_sqr = r * r;
	for (x, y, pixel) in image.enumerate_pixels_mut() {
		let dist_sqr = dist_sqr(x as f32, y as f32, cx, cy);
		if dist_sqr <= r_sqr {
			*pixel = lerp_rgba(
				easing(dist_sqr / r_sqr),
				Rgba([255, 0, 0, 255]), // center color
				Rgba([0; 4]), // edge color
			);
		} else {
			*pixel = Rgba([0; 4]);
		}
	}
	image
}

fn main() {
	fancy_circle(200, 200, 100, std::convert::identity).save("circle1.png").unwrap();
	fancy_circle(200, 200, 100, |x| x * x).save("circle2.png").unwrap();
}
1 Like

It looks like you didn't recognize the exact meaning of the English word "radius" (the word you're looking for is "diameter"). However, the radius variable is used consistently, so that's not a logic error in itself (although linguistic errors often lead to logic errors).

Unfortunately, the most important part of the algorithm, that calculate the curve of the circle, is almost not implemented at all. To recap, your code iterates through rows, and on each iteration it tries to adjust the one-pixel tall strip vec2 that you then append into the result buffer.

This is a legitimate way to draw a circle… but to do this correctly, you need to calculate how much longer or shorter vec2's painted part need to become. Your current code only changes it by a constant amount across all rows, which is set to 1 (PI / radius is 3.14... / 500 is 0.00628... ceiled to 1). Once you understand this, it should be no surprise that you got a rhombus.

The circle's outline can be efficiently calculated using the Bresenham's algorithm, whose micro-optimized version are called Michener's algorithm. This post was originally meant to be done in a timely manner, but I've got nerd-sniped by this and over-engineered the heck out of it; I'd say I had plenty of fun.

Playground (Mostly untested. Use at your own discretion.)

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.