cumulus3d_features/
gray.rs1use cumulus3d_core::{Error, Keypoint, Result};
35use std::path::Path;
36
37#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct GrayImage {
40 pub width: usize,
42 pub height: usize,
44 pub data: Vec<u8>,
46}
47
48impl GrayImage {
49 pub fn new(width: usize, height: usize, data: Vec<u8>) -> Result<Self> {
51 if data.len() != width * height {
52 return Err(Error::InvalidArgument(format!("회색 영상 크기 {width}x{height} 와 자료 길이 {} 불일치", data.len())));
53 }
54 Ok(Self { width, height, data })
55 }
56
57 pub fn filled(width: usize, height: usize, v: u8) -> Self {
59 Self { width, height, data: vec![v; width * height] }
60 }
61
62 pub fn from_f32(width: usize, height: usize, f: impl Fn(usize, usize) -> f32) -> Self {
64 let mut data = Vec::with_capacity(width * height);
65 for y in 0..height {
66 for x in 0..width {
67 data.push((f(x, y) * 255.0).round().clamp(0.0, 255.0) as u8);
68 }
69 }
70 Self { width, height, data }
71 }
72
73 #[inline]
74 pub fn get(&self, x: usize, y: usize) -> u8 {
76 self.data[y * self.width + x]
77 }
78
79 pub fn rotate_ccw(&self, k: u32) -> GrayImage {
81 let mut img = self.clone();
82 for _ in 0..(k % 4) {
83 img = img.rotate_ccw_once();
84 }
85 img
86 }
87
88 fn rotate_ccw_once(&self) -> GrayImage {
89 let (w, h) = (self.width, self.height);
90 let mut data = vec![0u8; w * h];
92 for ny in 0..w {
93 let oc = w - 1 - ny;
94 let row = &mut data[ny * h..(ny + 1) * h];
95 for (nx, v) in row.iter_mut().enumerate() {
96 *v = self.data[nx * w + oc];
97 }
98 }
99 GrayImage { width: h, height: w, data }
100 }
101
102 pub fn resized(&self, width: usize, height: usize) -> GrayImage {
105 if width == self.width && height == self.height {
106 return self.clone();
107 }
108 let buf = image::GrayImage::from_raw(self.width as u32, self.height as u32, self.data.clone()).expect("크기 불변식");
109 let out = image::imageops::resize(&buf, width as u32, height as u32, image::imageops::FilterType::Lanczos3);
110 GrayImage { width, height, data: out.into_raw() }
111 }
112}
113
114#[inline]
116pub fn rgb_to_gray(r: u8, g: u8, b: u8) -> u8 {
117 (0.2126f32 * r as f32 + 0.7152f32 * g as f32 + 0.0722f32 * b as f32).round().clamp(0.0, 255.0) as u8
118}
119
120pub fn to_gray(img: &image::DynamicImage) -> Result<GrayImage> {
122 use image::DynamicImage as D;
123 let (w, h) = (img.width() as usize, img.height() as usize);
124 let data = match img {
125 D::ImageLuma8(b) => b.as_raw().clone(),
126 D::ImageLumaA8(_) | D::ImageLuma16(_) | D::ImageLumaA16(_) => img.to_luma8().into_raw(),
127 D::ImageRgb8(b) => b.as_raw().chunks(3).map(|p| rgb_to_gray(p[0], p[1], p[2])).collect(),
128 _ => {
129 let c = img.color();
130 if c.channel_count() == 3 || c.channel_count() == 4 {
131 img.to_rgb8().as_raw().chunks(3).map(|p| rgb_to_gray(p[0], p[1], p[2])).collect()
132 } else {
133 return Err(Error::Unsupported(format!("채널 구성 {c:?}")));
134 }
135 }
136 };
137 GrayImage::new(w, h, data)
138}
139
140pub fn read_gray(path: &Path) -> Result<GrayImage> {
142 let reader = image::ImageReader::open(path)?.with_guessed_format().map_err(Error::Io)?;
143 let img = reader.decode().map_err(|e| Error::Format(format!("{}: 디코딩 실패: {e}", path.display())))?;
144 to_gray(&img)
145}
146
147pub fn limited_size(width: usize, height: usize, max_size: usize) -> (usize, usize) {
149 if width <= max_size && height <= max_size {
150 return (width, height);
151 }
152 let s = max_size as f64 / width.max(height) as f64;
153 (((width as f64 * s).round() as usize).max(1), ((height as f64 * s).round() as usize).max(1))
154}
155
156pub fn rotate_keypoint_ccw(kp: &Keypoint, k: u32, width: f32, height: f32) -> Keypoint {
158 let mut p = *kp;
159 let (mut w, mut h) = (width, height);
160 for _ in 0..(k % 4) {
161 p = Keypoint { x: p.y, y: w - p.x, a11: p.a21, a12: p.a22, a21: -p.a11, a22: -p.a12 };
162 std::mem::swap(&mut w, &mut h);
163 }
164 p
165}
166
167pub fn orientation_to_rotation(orientation: Option<u32>) -> u32 {
169 let g = orientation_gravity(orientation);
170 match g {
171 Some((gx, gy)) => {
172 let k = ((gy.atan2(gx) - std::f64::consts::FRAC_PI_2) / std::f64::consts::FRAC_PI_2).round() as i64;
173 k.rem_euclid(4) as u32
174 }
175 None => 0,
176 }
177}
178
179pub fn orientation_gravity(orientation: Option<u32>) -> Option<(f64, f64)> {
181 match orientation? {
182 1 => Some((0.0, 1.0)),
183 3 => Some((0.0, -1.0)),
184 6 => Some((1.0, 0.0)),
185 8 => Some((-1.0, 0.0)),
186 _ => None,
187 }
188}