Skip to main content

pic_scale/
scaler_f16.rs

1/*
2 * Copyright (c) Radzivon Bartoshyk. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without modification,
5 * are permitted provided that the following conditions are met:
6 *
7 * 1.  Redistributions of source code must retain the above copyright notice, this
8 * list of conditions and the following disclaimer.
9 *
10 * 2.  Redistributions in binary form must reproduce the above copyright notice,
11 * this list of conditions and the following disclaimer in the documentation
12 * and/or other materials provided with the distribution.
13 *
14 * 3.  Neither the name of the copyright holder nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29#![forbid(unsafe_code)]
30use crate::image_store::ImageStoreMut;
31use crate::plan::Resampling;
32use crate::scaler::ScalingOptions;
33use crate::validation::PicScaleError;
34use crate::{
35    CbCrF16ImageStore, ImageSize, ImageStoreScaling, PlanarF16ImageStore, RgbF16ImageStore,
36    RgbaF16ImageStore, Scaler,
37};
38use core::f16;
39use std::sync::Arc;
40
41/// Implements `f16` type support
42#[cfg_attr(docsrs, doc(cfg(feature = "nightly_f16")))]
43impl Scaler {
44    /// Creates a resampling plan for a single-channel (planar/grayscale) `f16` image.
45    ///
46    /// The `f16` variant of [`plan_planar_resampling`], suitable for half-precision
47    /// grayscale content such as HDR render targets or compressed texture data.
48    /// Filter weights are accumulated in `f32` to avoid precision loss during convolution.
49    ///
50    /// # Arguments
51    ///
52    /// - `source_size` — Dimensions of the input image.
53    /// - `target_size` — Desired dimensions of the output image.
54    ///
55    /// # Example
56    ///
57    /// ```rust,no_run,ignore
58    /// let plan = scaler.plan_planar_resampling_f16(source_size, target_size)?;
59    /// plan.resample(&store, &mut target_store)?;
60    /// ```
61    pub fn plan_planar_resampling_f16(
62        &self,
63        source_size: ImageSize,
64        target_size: ImageSize,
65    ) -> Result<Arc<Resampling<f16, 1>>, PicScaleError> {
66        self.plan_generic_resize::<f16, f32, 1>(source_size, target_size, 8)
67    }
68
69    /// Creates a resampling plan for a two-channel chroma (`CbCr`) `f16` image.
70    ///
71    /// The `f16` variant of [`plan_cbcr_resampling`], intended for half-precision chroma
72    /// planes of YCbCr content. Both channels are treated as independent signals with no
73    /// alpha relationship. Filter weights are accumulated in `f32` to avoid precision
74    /// loss during convolution.
75    ///
76    /// # Arguments
77    ///
78    /// - `source_size` — Dimensions of the input chroma plane.
79    /// - `target_size` — Desired dimensions of the output chroma plane.
80    ///
81    /// # Example
82    ///
83    /// ```rust,no_run,ignore
84    /// let plan = scaler.plan_cbcr_resampling_f16(source_size, target_size)?;
85    /// plan.resample(&cbcr_store, &mut target_cbcr_store)?;
86    /// ```
87    pub fn plan_cbcr_resampling_f16(
88        &self,
89        source_size: ImageSize,
90        target_size: ImageSize,
91    ) -> Result<Arc<Resampling<f16, 2>>, PicScaleError> {
92        self.plan_generic_resize::<f16, f32, 2>(source_size, target_size, 8)
93    }
94
95    /// Creates a resampling plan for a three-channel RGB `f16` image.
96    ///
97    /// The `f16` variant of [`plan_rgb_resampling`], suitable for half-precision color
98    /// images such as HDR render targets or OpenEXR content. All three channels are
99    /// resampled independently with no alpha relationship. Filter weights are accumulated
100    /// in `f32` to avoid precision loss during convolution.
101    ///
102    /// # Arguments
103    ///
104    /// - `source_size` — Dimensions of the input image.
105    /// - `target_size` — Desired dimensions of the output image.
106    ///
107    /// # Example
108    ///
109    /// ```rust,no_run,ignore
110    /// let plan = scaler.plan_rgb_resampling_f16(source_size, target_size)?;
111    /// plan.resample(&store, &mut target_store)?;
112    /// ```
113    pub fn plan_rgb_resampling_f16(
114        &self,
115        source_size: ImageSize,
116        target_size: ImageSize,
117    ) -> Result<Arc<Resampling<f16, 3>>, PicScaleError> {
118        self.plan_generic_resize::<f16, f32, 3>(source_size, target_size, 8)
119    }
120
121    /// Creates a resampling plan for a four-channel RGBA `f16` image.
122    ///
123    /// The `f16` variant of [`plan_rgba_resampling`]. Alpha premultiplication is always
124    /// applied — RGB channels are pre-multiplied by alpha before resampling and
125    /// un-multiplied afterward — regardless of the `premultiply_alpha` flag.
126    ///
127    /// # Arguments
128    ///
129    /// - `source_size` — Dimensions of the input image.
130    /// - `target_size` — Desired dimensions of the output image.
131    /// - `premultiply_alpha` — Whether to premultiply alpha before resampling.
132    ///
133    /// # Example
134    ///
135    /// ```rust,no_run,ignore
136    /// let plan = scaler.plan_rgba_resampling_f16(source_size, target_size, true)?;
137    /// plan.resample(&store, &mut target_store)?;
138    /// ```
139    pub fn plan_rgba_resampling_f16(
140        &self,
141        source_size: ImageSize,
142        target_size: ImageSize,
143        premultiply_alpha: bool,
144    ) -> Result<Arc<Resampling<f16, 4>>, PicScaleError> {
145        self.plan_generic_resize_with_alpha::<f16, f32, 4>(
146            source_size,
147            target_size,
148            8,
149            premultiply_alpha,
150        )
151    }
152}
153
154#[cfg_attr(docsrs, doc(cfg(feature = "nightly_f16")))]
155impl<'b> ImageStoreScaling<'b, f16, 1> for PlanarF16ImageStore<'b> {
156    fn scale(
157        &self,
158        store: &mut ImageStoreMut<'b, f16, 1>,
159        options: ScalingOptions,
160    ) -> Result<(), PicScaleError> {
161        let mut scaler = Scaler::new(options.resampling_function);
162        scaler.set_threading_policy(options.threading_policy);
163        let plan = scaler.plan_generic_resize(self.size(), store.size(), store.bit_depth)?;
164        plan.resample(self, store)
165    }
166}
167
168#[cfg_attr(docsrs, doc(cfg(feature = "nightly_f16")))]
169impl<'b> ImageStoreScaling<'b, f16, 2> for CbCrF16ImageStore<'b> {
170    fn scale(
171        &self,
172        store: &mut ImageStoreMut<'b, f16, 2>,
173        options: ScalingOptions,
174    ) -> Result<(), PicScaleError> {
175        let mut scaler = Scaler::new(options.resampling_function);
176        scaler.set_threading_policy(options.threading_policy);
177        let plan = scaler.plan_generic_resize(self.size(), store.size(), store.bit_depth)?;
178        plan.resample(self, store)
179    }
180}
181
182#[cfg_attr(docsrs, doc(cfg(feature = "nightly_f16")))]
183impl<'b> ImageStoreScaling<'b, f16, 3> for RgbF16ImageStore<'b> {
184    fn scale(
185        &self,
186        store: &mut ImageStoreMut<'b, f16, 3>,
187        options: ScalingOptions,
188    ) -> Result<(), PicScaleError> {
189        let mut scaler = Scaler::new(options.resampling_function);
190        scaler.set_threading_policy(options.threading_policy);
191        let plan = scaler.plan_generic_resize(self.size(), store.size(), store.bit_depth)?;
192        plan.resample(self, store)
193    }
194}
195
196#[cfg_attr(docsrs, doc(cfg(feature = "nightly_f16")))]
197impl<'b> ImageStoreScaling<'b, f16, 4> for RgbaF16ImageStore<'b> {
198    fn scale(
199        &self,
200        store: &mut ImageStoreMut<'b, f16, 4>,
201        options: ScalingOptions,
202    ) -> Result<(), PicScaleError> {
203        let mut scaler = Scaler::new(options.resampling_function);
204        scaler.set_threading_policy(options.threading_policy);
205        let plan = scaler.plan_generic_resize_with_alpha(
206            self.size(),
207            store.size(),
208            store.bit_depth,
209            options.premultiply_alpha,
210        )?;
211        plan.resample(self, store)
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::{ImageStore, ResamplingFunction, ThreadingPolicy};
219    use core::f16;
220
221    #[test]
222    fn check_rgba_f16_resizing_vertical() {
223        let image_width = 8;
224        let image_height = 8;
225        const CN: usize = 4;
226        let mut image = vec![0_f16; image_height * image_width * CN];
227        for dst in image.as_chunks_mut::<CN>().0.iter_mut() {
228            dst[0] = (124f32 / 255f32) as f16;
229            dst[1] = (41f32 / 255f32) as f16;
230            dst[2] = (99f32 / 255f32) as f16;
231            dst[3] = 1f32 as f16;
232        }
233        let mut scaler = Scaler::new(ResamplingFunction::Lanczos3);
234        scaler.set_threading_policy(ThreadingPolicy::Single);
235        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
236        let mut target_store = ImageStoreMut::<f16, 4>::alloc(image_width, image_height / 2);
237        let planned = scaler
238            .plan_rgba_resampling_f16(src_store.size(), target_store.size(), false)
239            .unwrap();
240        planned.resample(&src_store, &mut target_store).unwrap();
241        let target_data = target_store.buffer.borrow();
242
243        for dst in target_data.chunks_exact(CN) {
244            assert!(
245                (dst[0] as f32 * 255f32 - 124f32).abs() < 3f32,
246                "R channel mismatch: {}",
247                dst[0] as f32 * 255f32
248            );
249            assert!(
250                (dst[1] as f32 * 255f32 - 41f32).abs() < 3f32,
251                "G channel mismatch: {}",
252                dst[1] as f32 * 255f32
253            );
254            assert!(
255                (dst[2] as f32 * 255f32 - 99f32).abs() < 3f32,
256                "B channel mismatch: {}",
257                dst[2] as f32 * 255f32
258            );
259            assert!(
260                (dst[3] as f32 - 1f32).abs() < 0.01f32,
261                "A channel mismatch: {}",
262                dst[3] as f32
263            );
264        }
265    }
266
267    #[test]
268    fn check_rgba_f16_resizing_vertical_threading() {
269        let image_width = 8;
270        let image_height = 8;
271        const CN: usize = 4;
272        let mut image = vec![0_f16; image_height * image_width * CN];
273        for dst in image.as_chunks_mut::<CN>().0.iter_mut() {
274            dst[0] = (124f32 / 255f32) as f16;
275            dst[1] = (41f32 / 255f32) as f16;
276            dst[2] = (99f32 / 255f32) as f16;
277            dst[3] = 1f32 as f16;
278        }
279        let scaler = Scaler::new(ResamplingFunction::Lanczos3)
280            .set_threading_policy(ThreadingPolicy::Adaptive);
281        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
282        let mut target_store = ImageStoreMut::<f16, 4>::alloc(image_width, image_height / 2);
283        let planned = scaler
284            .plan_rgba_resampling_f16(src_store.size(), target_store.size(), false)
285            .unwrap();
286        planned.resample(&src_store, &mut target_store).unwrap();
287        let target_data = target_store.buffer.borrow();
288
289        for dst in target_data.chunks_exact(CN) {
290            assert!(
291                (dst[0] as f32 * 255f32 - 124f32).abs() < 3f32,
292                "R channel mismatch: {}",
293                dst[0] as f32 * 255f32
294            );
295            assert!(
296                (dst[1] as f32 * 255f32 - 41f32).abs() < 3f32,
297                "G channel mismatch: {}",
298                dst[1] as f32 * 255f32
299            );
300            assert!(
301                (dst[2] as f32 * 255f32 - 99f32).abs() < 3f32,
302                "B channel mismatch: {}",
303                dst[2] as f32 * 255f32
304            );
305            assert!(
306                (dst[3] as f32 - 1f32).abs() < 0.01f32,
307                "A channel mismatch: {}",
308                dst[3] as f32
309            );
310        }
311    }
312
313    #[test]
314    fn check_rgba_f16_nearest_vertical() {
315        let image_width = 255;
316        let image_height = 512;
317        const CN: usize = 4;
318        let mut image = vec![0_f16; image_height * image_width * CN];
319        for dst in image.as_chunks_mut::<CN>().0.iter_mut() {
320            dst[0] = (124f32 / 255f32) as f16;
321            dst[1] = (41f32 / 255f32) as f16;
322            dst[2] = (99f32 / 255f32) as f16;
323            dst[3] = (77f32 / 255f32) as f16;
324        }
325        let mut scaler = Scaler::new(ResamplingFunction::Nearest);
326        scaler.set_threading_policy(ThreadingPolicy::Single);
327        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
328        let mut target_store = ImageStoreMut::<f16, 4>::alloc(image_width, image_height / 2);
329        let planned = scaler
330            .plan_rgba_resampling_f16(src_store.size(), target_store.size(), false)
331            .unwrap();
332        planned.resample(&src_store, &mut target_store).unwrap();
333        let target_data = target_store.buffer.borrow();
334
335        for dst in target_data.chunks_exact(CN) {
336            assert!(
337                (dst[0] as f32 * 255f32 - 124f32).abs() < 3f32,
338                "R channel mismatch: {}",
339                dst[0] as f32 * 255f32
340            );
341            assert!(
342                (dst[1] as f32 * 255f32 - 41f32).abs() < 3f32,
343                "G channel mismatch: {}",
344                dst[1] as f32 * 255f32
345            );
346            assert!(
347                (dst[2] as f32 * 255f32 - 99f32).abs() < 3f32,
348                "B channel mismatch: {}",
349                dst[2] as f32 * 255f32
350            );
351            assert!(
352                (dst[3] as f32 * 255f32 - 77f32).abs() < 3f32,
353                "A channel mismatch: {}",
354                dst[3] as f32 * 255f32
355            );
356        }
357    }
358
359    #[test]
360    fn check_rgba_f16_nearest_vertical_threading() {
361        let image_width = 255;
362        let image_height = 512;
363        const CN: usize = 4;
364        let mut image = vec![0_f16; image_height * image_width * CN];
365        for dst in image.as_chunks_mut::<CN>().0.iter_mut() {
366            dst[0] = (124f32 / 255f32) as f16;
367            dst[1] = (41f32 / 255f32) as f16;
368            dst[2] = (99f32 / 255f32) as f16;
369            dst[3] = (77f32 / 255f32) as f16;
370        }
371        let scaler = Scaler::new(ResamplingFunction::Nearest)
372            .set_threading_policy(ThreadingPolicy::Adaptive);
373        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
374        let mut target_store = ImageStoreMut::<f16, 4>::alloc(image_width, image_height / 2);
375        let planned = scaler
376            .plan_rgba_resampling_f16(src_store.size(), target_store.size(), false)
377            .unwrap();
378        planned.resample(&src_store, &mut target_store).unwrap();
379        let target_data = target_store.buffer.borrow();
380
381        for dst in target_data.chunks_exact(CN) {
382            assert!(
383                (dst[0] as f32 * 255f32 - 124f32).abs() < 3f32,
384                "R channel mismatch: {}",
385                dst[0] as f32 * 255f32
386            );
387            assert!(
388                (dst[1] as f32 * 255f32 - 41f32).abs() < 3f32,
389                "G channel mismatch: {}",
390                dst[1] as f32 * 255f32
391            );
392            assert!(
393                (dst[2] as f32 * 255f32 - 99f32).abs() < 3f32,
394                "B channel mismatch: {}",
395                dst[2] as f32 * 255f32
396            );
397            assert!(
398                (dst[3] as f32 * 255f32 - 77f32).abs() < 3f32,
399                "A channel mismatch: {}",
400                dst[3] as f32 * 255f32
401            );
402        }
403    }
404
405    #[test]
406    fn check_rgb_f16_resizing_vertical() {
407        let image_width = 8;
408        let image_height = 8;
409        const CN: usize = 3;
410        let mut image = vec![0_f16; image_height * image_width * CN];
411        for dst in image.as_chunks_mut::<CN>().0.iter_mut() {
412            dst[0] = (124f32 / 255f32) as f16;
413            dst[1] = (41f32 / 255f32) as f16;
414            dst[2] = (99f32 / 255f32) as f16;
415        }
416        let mut scaler = Scaler::new(ResamplingFunction::Lanczos3);
417        scaler.set_threading_policy(ThreadingPolicy::Single);
418        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
419        let mut target_store = ImageStoreMut::<f16, 3>::alloc(image_width, image_height / 2);
420        let planned = scaler
421            .plan_rgb_resampling_f16(src_store.size(), target_store.size())
422            .unwrap();
423        planned.resample(&src_store, &mut target_store).unwrap();
424        let target_data = target_store.buffer.borrow();
425
426        for dst in target_data.as_chunks::<CN>().0.iter() {
427            assert!(
428                (dst[0] as f32 * 255f32 - 124f32).abs() < 3f32,
429                "R channel mismatch: {}",
430                dst[0] as f32 * 255f32
431            );
432            assert!(
433                (dst[1] as f32 * 255f32 - 41f32).abs() < 3f32,
434                "G channel mismatch: {}",
435                dst[1] as f32 * 255f32
436            );
437            assert!(
438                (dst[2] as f32 * 255f32 - 99f32).abs() < 3f32,
439                "B channel mismatch: {}",
440                dst[2] as f32 * 255f32
441            );
442        }
443    }
444
445    #[test]
446    fn check_cbcr_f16_resizing_vertical() {
447        let image_width = 8;
448        let image_height = 8;
449        const CN: usize = 2;
450        let mut image = vec![0_f16; image_height * image_width * CN];
451        for dst in image.as_chunks_mut::<CN>().0.iter_mut() {
452            dst[0] = (124f32 / 255f32) as f16;
453            dst[1] = (41f32 / 255f32) as f16;
454        }
455        let mut scaler = Scaler::new(ResamplingFunction::Lanczos3);
456        scaler.set_threading_policy(ThreadingPolicy::Single);
457        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
458        let mut target_store = ImageStoreMut::<f16, 2>::alloc(image_width, image_height / 2);
459        let planned = scaler
460            .plan_cbcr_resampling_f16(src_store.size(), target_store.size())
461            .unwrap();
462        planned.resample(&src_store, &mut target_store).unwrap();
463        let target_data = target_store.buffer.borrow();
464
465        for dst in target_data.as_chunks::<CN>().0.iter() {
466            assert!(
467                (dst[0] as f32 * 255f32 - 124f32).abs() < 3f32,
468                "R channel mismatch: {}",
469                dst[0] as f32 * 255f32
470            );
471            assert!(
472                (dst[1] as f32 * 255f32 - 41f32).abs() < 3f32,
473                "G channel mismatch: {}",
474                dst[1] as f32 * 255f32
475            );
476        }
477    }
478
479    #[test]
480    fn check_planar_f16_resizing_vertical() {
481        let image_width = 8;
482        let image_height = 8;
483        const CN: usize = 1;
484        let mut image = vec![0_f16; image_height * image_width * CN];
485        for dst in image.as_chunks_mut::<CN>().0.iter_mut() {
486            dst[0] = (124f32 / 255f32) as f16;
487        }
488        let mut scaler = Scaler::new(ResamplingFunction::Lanczos3);
489        scaler.set_threading_policy(ThreadingPolicy::Single);
490        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
491        let mut target_store = ImageStoreMut::<f16, 1>::alloc(image_width, image_height / 2);
492        let planned = scaler
493            .plan_planar_resampling_f16(src_store.size(), target_store.size())
494            .unwrap();
495        planned.resample(&src_store, &mut target_store).unwrap();
496        let target_data = target_store.buffer.borrow();
497
498        for dst in target_data.as_chunks::<CN>().0.iter() {
499            assert!(
500                (dst[0] as f32 * 255f32 - 124f32).abs() < 3f32,
501                "R channel mismatch: {}",
502                dst[0] as f32 * 255f32
503            );
504        }
505    }
506}