Search code examples
image-processingrust

Get diagonal pixels from rectangular image


I have a 1920x1080 input image and want to analyze luminescence distribution over the diagonal. I could use just the middle row and "stretch" it as needed as an alternative, but I would like to stick with the actual diagonal if possible.

My current approach picks pixels calculated via dimension factors over the diagonal based on Pythagoras. This results in around 170 duplicates, I assume because of rounding.

Is there a proper way to achieve this? The input is always going to be in this format but of variable resolution.

let num_rows = img.height();
let num_cols = img.width();
let diag_steps = ((num_cols.pow(2) + num_rows.pow(2)) as f32)
        .sqrt()
        .floor() as u32;
let col_factor = num_cols as f32 / diag_steps as f32;
let row_factor = num_rows as f32 / diag_steps as f32;
let mut diag_values = vec![];

for s in 0..diag_steps {
    let r = ((s as f32 * row_factor).round() as u32).min(num_rows - 1);
    let c = ((s as f32 * col_factor).round() as u32).min(num_cols - 1);
    diag_values.push(c, r);
}

This approach is purely based on what I thought should work and I am using the image crate for reading/writing images. I found a related question on SO but the answer does not help me, I need the actual diagonal through the middle from top-right to bottom-left, not a diagonal where one sets Y = X.

I also do averaging over pixels around each coordinate to account a bit for noise but that's not important for the duplicates.

I tried sampling the image like listed in the code block and expected to get pixels along the diagonal of an image. I got the expected amount of 2202 pixels, but these contained duplicates due to rounding and I assume this could lead to further problems later on.


Solution

  • You can step by the aspect ratio, like so:

    fn greatest_common_factor(rhs: u32, lhs: u32) -> u32 {
        todo!("Implement me or import a crate for me")
    }
    
    fn simplify_fraction(rhs: u32, lhs: u32) -> (u32, u32) {
        let gcf = greatest_common_factor(rhs, lhs);
        (rhs / gcf, lhs / gcf)
    }
    
    fn top_left_to_bottom_right<I>(image: &I) -> impl Iterator<Item = I::Pixel> + '_
    where
        I: GenericImageView,
    {
        let width = image.width();
        let height = image.height();
    
        let (run, rise) = simplify_fraction(width, height);
    
        let x = (0..width).into_iter().step_by(run as usize);
        //start from top right instead?
        //let x = (0..width).into_iter().step_by(run as usize).rev();
        let y = (0..height).into_iter().step_by(rise as usize);
    
        let coords = x.zip(y);
    
        coords.map(|(x, y)| image.get_pixel(x, y))
    }
    

    These are the exact pixels that fall on the exact diagonal line.

    This obviously has the disadvantage of using whole numbers for the aspect ratio. The consequence is an image of 1920x1080 will have an aspect ratio of 16/9. Because we step by 16 and 9 pixels, respectively, we are skipping some pixels.

    This function outputs 120 pixels for a 1920x1080 image (the gcf). If this is undesirable, you will need to tackle sub pixel manipulation and computation with mixed numbers (E.g. what is 1.5 pixels? Does the image crate support this out of the box? Would you round floats to integers and not care about uniformity?). You can then, either extrapolate points between your gcf number of points, or calculate your aspect ratio as a ratio of floats and use slopes as shown above.