av_denoise/nlmeans/motion/
mod.rs1mod analyse;
2mod compensate;
3mod pyramid;
4
5pub(crate) use analyse::run_analyse;
6pub(crate) use compensate::run_compensate;
7use cubecl::prelude::*;
8use cubecl::server::Handle;
9pub(crate) use pyramid::{pyramid_pixels_per_frame, run_pyramid_build};
10
11#[non_exhaustive]
18#[derive(Debug, Default, Clone, Copy, PartialEq)]
19pub enum MotionCompensationMode {
20 #[default]
21 None,
22 Mvtools {
23 blksize: u32,
26 overlap: u32,
31 search_radius: u32,
35 pyramid_levels: u32,
39 },
40}
41
42pub const DEFAULT_BLKSIZE: u32 = 16;
45pub const DEFAULT_OVERLAP: u32 = 8;
47pub const DEFAULT_SEARCH_RADIUS: u32 = 4;
50pub const DEFAULT_PYRAMID_LEVELS: u32 = 2;
54
55pub const MAX_PYRAMID_LEVELS: u32 = 3;
59pub const MAX_SEARCH_RADIUS: u32 = 8;
62pub const MAX_BLKSIZE: u32 = 32;
65
66impl MotionCompensationMode {
67 pub fn mvtools_default() -> Self {
69 Self::Mvtools {
70 blksize: DEFAULT_BLKSIZE,
71 overlap: DEFAULT_OVERLAP,
72 search_radius: DEFAULT_SEARCH_RADIUS,
73 pyramid_levels: DEFAULT_PYRAMID_LEVELS,
74 }
75 }
76
77 pub(crate) fn is_active(self) -> bool {
79 !matches!(self, Self::None)
80 }
81
82 pub fn validate(&self) -> Result<(), anyhow::Error> {
84 let Self::Mvtools {
85 blksize,
86 overlap,
87 search_radius,
88 pyramid_levels,
89 } = *self
90 else {
91 return Ok(());
92 };
93
94 if blksize < 4 {
95 anyhow::bail!("motion-compensation blksize={blksize} is too small; minimum is 4 pixels per side");
96 }
97 if blksize > MAX_BLKSIZE {
98 anyhow::bail!(
99 "motion-compensation blksize={blksize} exceeds the supported maximum ({MAX_BLKSIZE})"
100 );
101 }
102 if blksize % 2 != 0 {
103 anyhow::bail!(
104 "motion-compensation blksize={blksize} must be even so the /2 coarse level is well-defined"
105 );
106 }
107 if overlap >= blksize {
108 anyhow::bail!(
109 "motion-compensation overlap={overlap} must be strictly less than blksize ({blksize}) so step > 0"
110 );
111 }
112 if search_radius == 0 || search_radius > MAX_SEARCH_RADIUS {
113 anyhow::bail!(
114 "motion-compensation search_radius={search_radius} must be in 1..={MAX_SEARCH_RADIUS}"
115 );
116 }
117 if pyramid_levels == 0 || pyramid_levels > MAX_PYRAMID_LEVELS {
118 anyhow::bail!(
119 "motion-compensation pyramid_levels={pyramid_levels} must be in 1..={MAX_PYRAMID_LEVELS}"
120 );
121 }
122
123 Ok(())
124 }
125}
126
127#[derive(Debug, Clone)]
138pub(crate) struct MotionCtx {
139 pub blksize: u32,
140 pub step: u32,
141 pub search_radius: u32,
142 pub pyramid_levels: u32,
143 pub blocks_x: u32,
144 pub blocks_y: u32,
145}
146
147impl MotionCtx {
148 pub fn new(mode: MotionCompensationMode, width: u32, height: u32) -> Option<Self> {
149 let MotionCompensationMode::Mvtools {
150 blksize,
151 overlap,
152 search_radius,
153 pyramid_levels,
154 } = mode
155 else {
156 return None;
157 };
158
159 let step = blksize - overlap;
160 let blocks_x = width.div_ceil(step).max(1);
161 let blocks_y = height.div_ceil(step).max(1);
162
163 Some(Self {
164 blksize,
165 step,
166 search_radius,
167 pyramid_levels,
168 blocks_x,
169 blocks_y,
170 })
171 }
172
173 pub fn mv_slots_per_neighbour(&self) -> usize {
175 (self.blocks_x * self.blocks_y) as usize
176 }
177}
178
179#[allow(clippy::too_many_arguments)]
182pub(crate) fn build_pyramid_for_slot<R: Runtime>(
183 client: &ComputeClient<R>,
184 mc: &MotionCtx,
185 width: u32,
186 height: u32,
187 frame_count: u32,
188 slot: u32,
189 full_res: &Handle,
190 pyramid: &Handle,
191 stored_ch: u32,
192) -> Result<(), anyhow::Error> {
193 if mc.pyramid_levels <= 1 {
194 return Ok(());
195 }
196 run_pyramid_build::<R>(
197 client,
198 mc,
199 width,
200 height,
201 frame_count,
202 slot,
203 full_res,
204 pyramid,
205 stored_ch,
206 )
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn none_is_inactive() {
215 let m = MotionCompensationMode::None;
216 assert!(!m.is_active());
217 m.validate().unwrap();
218 }
219
220 #[test]
221 fn mvtools_default_is_active() {
222 let m = MotionCompensationMode::mvtools_default();
223 assert!(m.is_active());
224 m.validate().unwrap();
225 }
226
227 #[test]
228 fn validate_rejects_tiny_blksize() {
229 let m = MotionCompensationMode::Mvtools {
230 blksize: 2,
231 overlap: 0,
232 search_radius: 4,
233 pyramid_levels: 2,
234 };
235 assert!(m.validate().is_err());
236 }
237
238 #[test]
239 fn validate_rejects_odd_blksize() {
240 let m = MotionCompensationMode::Mvtools {
241 blksize: 9,
242 overlap: 0,
243 search_radius: 4,
244 pyramid_levels: 2,
245 };
246 assert!(m.validate().is_err());
247 }
248
249 #[test]
250 fn validate_rejects_overlap_equal_to_blksize() {
251 let m = MotionCompensationMode::Mvtools {
252 blksize: 16,
253 overlap: 16,
254 search_radius: 4,
255 pyramid_levels: 2,
256 };
257 assert!(m.validate().is_err());
259 }
260
261 #[test]
262 fn validate_accepts_half_overlap() {
263 let m = MotionCompensationMode::Mvtools {
264 blksize: 16,
265 overlap: 8,
266 search_radius: 4,
267 pyramid_levels: 2,
268 };
269 m.validate().unwrap();
270 }
271
272 #[test]
273 fn validate_rejects_zero_search_radius() {
274 let m = MotionCompensationMode::Mvtools {
275 blksize: 16,
276 overlap: 4,
277 search_radius: 0,
278 pyramid_levels: 2,
279 };
280 assert!(m.validate().is_err());
281 }
282
283 #[test]
284 fn validate_rejects_zero_pyramid_levels() {
285 let m = MotionCompensationMode::Mvtools {
286 blksize: 16,
287 overlap: 4,
288 search_radius: 4,
289 pyramid_levels: 0,
290 };
291 assert!(m.validate().is_err());
292 }
293
294 #[test]
295 fn motion_ctx_blocks_match_step() {
296 let mode = MotionCompensationMode::Mvtools {
297 blksize: 16,
298 overlap: 8,
299 search_radius: 4,
300 pyramid_levels: 2,
301 };
302 let ctx = MotionCtx::new(mode, 1920, 1080).unwrap();
303 assert_eq!(ctx.step, 8);
304 assert_eq!(ctx.blocks_x, 1920u32.div_ceil(8));
305 assert_eq!(ctx.blocks_y, 1080u32.div_ceil(8));
306 }
307}