Skip to main content

scan_core/
image_utils.rs

1use image::{GrayImage, Luma};
2use crate::transform::perspective::PerspectiveTransform;
3
4/// Apply perspective transform to the entire image.
5/// Maps `src_img` to a destination image of size `width` x `height`
6/// using the transform `pt` (which maps Src -> Dst).
7/// We iterate Dst pixels, map backwards to Src using `transform_inverse` (coeff_inv), and sample.
8pub fn warp_perspective(src_img: &GrayImage, pt: &PerspectiveTransform, width: u32, height: u32) -> GrayImage {
9    let mut dst = GrayImage::new(width, height);
10    let src_w = src_img.width() as f32;
11    let src_h = src_img.height() as f32;
12
13    for y in 0..height {
14        for x in 0..width {
15            let src_pt = pt.transform_inverse(x as f32, y as f32);
16            let sx = src_pt.x;
17            let sy = src_pt.y;
18            
19            if sx >= 0.0 && sx < src_w - 1.0 && sy >= 0.0 && sy < src_h - 1.0 {
20                // Bilinear interpolation
21                let x0 = sx.floor() as u32;
22                let y0 = sy.floor() as u32;
23                
24                let dx = sx - x0 as f32;
25                let dy = sy - y0 as f32;
26                
27                let p00 = src_img.get_pixel(x0, y0)[0] as f32;
28                let p10 = src_img.get_pixel(x0 + 1, y0)[0] as f32;
29                let p01 = src_img.get_pixel(x0, y0 + 1)[0] as f32;
30                let p11 = src_img.get_pixel(x0 + 1, y0 + 1)[0] as f32;
31                
32                let val = (1.0 - dy) * ((1.0 - dx) * p00 + dx * p10) 
33                        + dy * ((1.0 - dx) * p01 + dx * p11);
34                
35                dst.put_pixel(x, y, Luma([val as u8]));
36            }
37        }
38    }
39
40    dst
41}
42
43/// Convert a GrayImage to a Base64 JPEG with a quadrilateral and optional anchors drawn on it.
44/// If `transform` is provided, anchors are projected from template space (relative to width=100) to image space.
45pub fn generate_marked_image(
46    gray: &image::GrayImage, 
47    quad: &[crate::Point], 
48    anchors: Option<&[crate::AnchorConfig]>,
49    marker_outlines: Option<&[Vec<crate::Point>]>,
50    transform: Option<&crate::transform::perspective::PerspectiveTransform>
51) -> String {
52    use imageproc::drawing::draw_line_segment_mut;
53    use image::Rgb;
54    use base64::Engine as _;
55
56    let (w, h) = gray.dimensions();
57    let mut rgb = image::RgbImage::new(w, h);
58    for y in 0..h {
59        for x in 0..w {
60            let v = gray.get_pixel(x, y)[0];
61            rgb.put_pixel(x, y, Rgb([v, v, v]));
62        }
63    }
64
65    let red = Rgb([255u8, 0, 0]);
66    let thickness = (w as f32 * 0.003).max(3.0) as i32;
67
68    for t in -(thickness / 2)..(thickness / 2) {
69        let offset = t as f32;
70        for i in 0..4 {
71            let p1 = quad[i];
72            let p2 = quad[(i + 1) % 4];
73            draw_line_segment_mut(&mut rgb, (p1.x + offset, p1.y), (p2.x + offset, p2.y), red);
74            draw_line_segment_mut(&mut rgb, (p1.x, p1.y + offset), (p2.x, p2.y + offset), red);
75        }
76    }
77
78    // Draw anchors if provided
79    if let Some(anchs) = anchors {
80        let purple = Rgb([200u8, 0, 200]); // Brighter purple
81        use imageproc::drawing::{draw_hollow_rect_mut, draw_line_segment_mut};
82        use imageproc::rect::Rect;
83
84        for anchor in anchs {
85            if let Some(pt) = transform {
86                // Project 4 corners of the anchor
87                let p_tl = pt.transform_inverse(anchor.x, anchor.y);
88                let p_tr = pt.transform_inverse(anchor.x + anchor.width, anchor.y);
89                let p_br = pt.transform_inverse(anchor.x + anchor.width, anchor.y + anchor.height);
90                let p_bl = pt.transform_inverse(anchor.x, anchor.y + anchor.height);
91                
92                let pts = [p_tl, p_tr, p_br, p_bl];
93                for t in -(thickness / 2)..(thickness / 2) {
94                    let offset = t as f32;
95                    for i in 0..4 {
96                        let p1 = pts[i];
97                        let p2 = pts[(i + 1) % 4];
98                        draw_line_segment_mut(&mut rgb, (p1.x + offset, p1.y), (p2.x + offset, p2.y), purple);
99                        draw_line_segment_mut(&mut rgb, (p1.x, p1.y + offset), (p2.x, p2.y + offset), purple);
100                    }
101                }
102            } else {
103                // Fallback: simple scaling (no perspective projection)
104                let scale = w as f32 / 100.0;
105                let ax = anchor.x * scale;
106                let ay = anchor.y * scale;
107                let aw = anchor.width * scale;
108                let ah = anchor.height * scale;
109
110                for t in 0..thickness {
111                    let rect = Rect::at((ax - t as f32/2.0) as i32, (ay - t as f32/2.0) as i32)
112                        .of_size((aw + t as f32) as u32, (ah + t as f32) as u32);
113                    draw_hollow_rect_mut(&mut rgb, rect, purple);
114                }
115            }
116        }
117    }
118
119    // Draw marker outlines if provided (usually in ArUco mode)
120    if let Some(outlines) = marker_outlines {
121        let green = Rgb([0u8, 255, 0]);
122        for outline in outlines {
123            for i in 0..outline.len() {
124                let p1 = outline[i];
125                let p2 = outline[(i + 1) % outline.len()];
126                draw_line_segment_mut(&mut rgb, (p1.x, p1.y), (p2.x, p2.y), green);
127            }
128        }
129    }
130
131    let mut buffer = std::io::Cursor::new(Vec::new());
132    let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buffer, 85);
133    image::ImageEncoder::write_image(
134        encoder, rgb.as_raw(), w, h, image::ExtendedColorType::Rgb8
135    ).ok();
136
137    base64::engine::general_purpose::STANDARD.encode(buffer.get_ref())
138}
139
140pub fn to_png_bytes(img: &GrayImage) -> anyhow::Result<Vec<u8>> {
141    let mut buffer = std::io::Cursor::new(Vec::new());
142    img.write_to(&mut buffer, image::ImageFormat::Png)?;
143    Ok(buffer.into_inner())
144}
145
146pub fn encode_debug_image(img: &GrayImage) -> Option<String> {
147    use base64::Engine as _;
148    to_png_bytes(img).ok().map(|b| base64::engine::general_purpose::STANDARD.encode(b))
149}