1use super::*;
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
7pub struct RgbResize {
8 pub height: usize,
10 pub width: usize,
12 pub region: [usize; 4],
14}
15impl RgbResize {
16 pub fn new(height: usize, width: usize) -> Self {
18 Self {
19 height,
20 width,
21 region: [0, 0, width, height],
22 }
23 }
24
25 pub fn letterbox(
28 source_height: usize,
29 source_width: usize,
30 height: usize,
31 width: usize,
32 centered: bool,
33 ) -> Result<Self> {
34 if [source_height, source_width, height, width].contains(&0) {
35 return Err(Error::Message("resize dimensions must be nonzero".into()));
36 }
37 let horizontal = (width as u128) * source_height as u128;
38 let vertical = (height as u128) * source_width as u128;
39 let (w, h) = if horizontal <= vertical {
40 (width, (horizontal / source_width as u128) as usize)
41 } else {
42 ((vertical / source_height as u128) as usize, height)
43 };
44 if w == 0 || h == 0 {
45 return Err(Error::Message("image aspect ratio is too extreme".into()));
46 }
47 Ok(Self {
48 height,
49 width,
50 region: [
51 if centered { (width - w) / 2 } else { 0 },
52 if centered { (height - h) / 2 } else { 0 },
53 w,
54 h,
55 ],
56 })
57 }
58}
59
60fn coefficients(source: usize, output: usize) -> (Vec<u8>, usize) {
64 let weights: Vec<_> = (0..output)
65 .map(|i| {
66 let x = (i as f64 + 0.5) * (source as f64 / output as f64) - 0.5;
67 let start = x.floor();
68 if start < 0. {
69 (0, 0, 1., 0.)
70 } else if start >= (source - 1) as f64 {
71 (source - 1, source - 1, 1., 0.)
72 } else {
73 (
74 start as usize,
75 start as usize + 1,
76 1. - (x - start),
77 x - start,
78 )
79 }
80 })
81 .collect();
82 let max = weights.iter().map(|w| w.2.max(w.3)).fold(0f64, f64::max);
83 let precision = (0..22)
84 .find(|p| (max * (1u32 << (p + 1)) as f64).round() >= 32768.)
85 .unwrap_or(21);
86 let scale = (1u32 << precision) as f64;
87 let bytes = weights
88 .iter()
89 .flat_map(|&(a, b, x, y)| {
90 [
91 a as i32,
92 b as i32,
93 (x * scale).round() as i32,
94 (y * scale).round() as i32,
95 ]
96 .into_iter()
97 .flat_map(i32::to_le_bytes)
98 })
99 .collect();
100 (bytes, precision)
101}
102
103impl ImageOps {
104 pub fn prepare_resize_rgb(
107 &self,
108 input: &TensorDesc,
109 resize: RgbResize,
110 ) -> Result<Arc<PreparedModel>> {
111 self.resize_plans
112 .get_or_prepare((input.clone(), resize), || {
113 self.resize_rgb_fragment(input, resize)?.prepare(3)
114 })
115 }
116
117 pub fn resize_rgb_fragment(
119 &self,
120 input: &TensorDesc,
121 resize: RgbResize,
122 ) -> Result<ModelFragment> {
123 let shape = input.shape();
124 let [left, top, width, height] = resize.region;
125 if input.dtype() != DType::U8
126 || input.layout() != Layout::Nhwc
127 || !input.is_contiguous()
128 || input.is_empty()
129 || shape.len() != 4
130 || shape[3] != 3
131 || input.elements() > i32::MAX as usize
132 || width == 0
133 || height == 0
134 || left.checked_add(width).is_none_or(|v| v > resize.width)
135 || top.checked_add(height).is_none_or(|v| v > resize.height)
136 {
137 return Err(Error::Message(
138 "invalid NHWC RGB resize dimensions or rectangle".into(),
139 ));
140 }
141 let output = TensorDesc::new(DType::U8, vec![shape[0], resize.height, resize.width, 3])?
142 .with_layout(Layout::Nhwc)?;
143 let intermediate = TensorDesc::new(DType::U8, vec![shape[0], height, shape[2], 3])?;
144 if output.elements() > i32::MAX as usize || intermediate.elements() > i32::MAX as usize {
145 return Err(Error::Message(
146 "resize exceeds kernel indexing limits".into(),
147 ));
148 }
149 let mut model = ModelSession::in_context(&self.context)?;
150 let src = model.allocate(input.bytes())?;
151 let tmp = model.allocate(intermediate.bytes())?;
152 let dst = model.allocate(output.bytes())?;
153 let mut commands = Vec::new();
154 for (vertical, from, to, sh, sw, dh, dw, x, y, innerw, innerh, elements) in [
155 (
156 true,
157 src,
158 tmp,
159 shape[1],
160 shape[2],
161 height,
162 shape[2],
163 0,
164 0,
165 shape[2],
166 height,
167 intermediate.elements(),
168 ),
169 (
170 false,
171 tmp,
172 dst,
173 height,
174 shape[2],
175 resize.height,
176 resize.width,
177 left,
178 top,
179 width,
180 height,
181 output.elements(),
182 ),
183 ] {
184 let axis = if vertical { innerh } else { innerw };
185 let (bytes, precision) = coefficients(if vertical { sh } else { sw }, axis);
186 let table = model.weight(&bytes)?;
187 let mut source = include_str!("resize_rgb.loom")
188 .replace("@VERTICAL@", if vertical { "true" } else { "false" });
189 for (name, value) in [
190 ("COUNT", elements),
191 ("LAST", elements - 1),
192 ("INPUT", shape[0] * sh * sw * 3),
193 ("INPUT_LAST", shape[0] * sh * sw * 3 - 1),
194 ("COEFFS", axis * 4),
195 ("COEFFS_LAST", axis * 4 - 1),
196 ("AXIS_LAST", if vertical { sh - 1 } else { sw - 1 }),
197 ("GRID", elements.div_ceil(256)),
198 ("SH", sh),
199 ("SW", sw),
200 ("DH", dh),
201 ("DW", dw),
202 ("LEFT", x),
203 ("TOP", y),
204 ("RIGHT", x + innerw),
205 ("BOTTOM", y + innerh),
206 ("PRECISION", precision),
207 ("ROUND", 1 << (precision - 1)),
208 ] {
209 source = source.replace(&format!("@{name}@"), &value.to_string());
210 }
211 let kernel =
214 unsafe { model.compile(&[(&source, Specialization::new("resize_rgb"))])? }[0];
215 commands.push(Command::Dispatch(Dispatch::indices(
216 kernel,
217 [0],
218 [elements.div_ceil(256) as u32, 1, 1],
219 vec![from.read(), table.read(), to.write()],
220 )));
221 }
222 unsafe {
223 model.freeze(&self.context)?.fragment(
224 &commands,
225 &[(src, input.clone())],
226 &[(dst, output)],
227 )
228 }
229 }
230
231 pub fn resize_rgb(&self, image: &DeviceTensor, resize: RgbResize) -> Result<Inference> {
234 self.context.validate(image)?;
235 self.prepare_resize_rgb(image.desc(), resize)?
236 .submit(std::slice::from_ref(image))
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 #[test]
244 fn letterbox_geometry_is_checked() {
245 assert_eq!(
246 RgbResize::letterbox(100, 200, 640, 640, false)
247 .unwrap()
248 .region,
249 [0, 0, 640, 320]
250 );
251 assert_eq!(
252 RgbResize::letterbox(200, 100, 640, 640, true)
253 .unwrap()
254 .region,
255 [160, 0, 320, 640]
256 );
257 assert!(RgbResize::letterbox(0, 100, 640, 640, false).is_err());
258 assert!(RgbResize::letterbox(1, 10000, 1, 1, false).is_err());
259 }
260}