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::{AlphaPlanner, DefaultPlanner, 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        DefaultPlanner::plan_generic_resize::<f16, f32, 1>(self, 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        DefaultPlanner::plan_generic_resize::<f16, f32, 2>(self, 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        DefaultPlanner::plan_generic_resize::<f16, f32, 3>(self, 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        AlphaPlanner::plan_generic_resize_with_alpha::<f16, f32, 4>(
146            self,
147            source_size,
148            target_size,
149            8,
150            premultiply_alpha,
151        )
152    }
153}
154
155#[cfg_attr(docsrs, doc(cfg(feature = "nightly_f16")))]
156impl<'b> ImageStoreScaling<'b, f16, 1> for PlanarF16ImageStore<'b> {
157    fn scale(
158        &self,
159        store: &mut ImageStoreMut<'b, f16, 1>,
160        options: ScalingOptions,
161    ) -> Result<(), PicScaleError> {
162        let scaler =
163            Scaler::new(options.resampling_function).set_threading_policy(options.threading_policy);
164        let plan = DefaultPlanner::plan_generic_resize(
165            &scaler,
166            self.size(),
167            store.size(),
168            store.bit_depth,
169        )?;
170        plan.resample(self, store)
171    }
172}
173
174#[cfg_attr(docsrs, doc(cfg(feature = "nightly_f16")))]
175impl<'b> ImageStoreScaling<'b, f16, 2> for CbCrF16ImageStore<'b> {
176    fn scale(
177        &self,
178        store: &mut ImageStoreMut<'b, f16, 2>,
179        options: ScalingOptions,
180    ) -> Result<(), PicScaleError> {
181        let scaler =
182            Scaler::new(options.resampling_function).set_threading_policy(options.threading_policy);
183        let plan = DefaultPlanner::plan_generic_resize(
184            &scaler,
185            self.size(),
186            store.size(),
187            store.bit_depth,
188        )?;
189        plan.resample(self, store)
190    }
191}
192
193#[cfg_attr(docsrs, doc(cfg(feature = "nightly_f16")))]
194impl<'b> ImageStoreScaling<'b, f16, 3> for RgbF16ImageStore<'b> {
195    fn scale(
196        &self,
197        store: &mut ImageStoreMut<'b, f16, 3>,
198        options: ScalingOptions,
199    ) -> Result<(), PicScaleError> {
200        let scaler =
201            Scaler::new(options.resampling_function).set_threading_policy(options.threading_policy);
202        let plan = DefaultPlanner::plan_generic_resize(
203            &scaler,
204            self.size(),
205            store.size(),
206            store.bit_depth,
207        )?;
208        plan.resample(self, store)
209    }
210}
211
212#[cfg_attr(docsrs, doc(cfg(feature = "nightly_f16")))]
213impl<'b> ImageStoreScaling<'b, f16, 4> for RgbaF16ImageStore<'b> {
214    fn scale(
215        &self,
216        store: &mut ImageStoreMut<'b, f16, 4>,
217        options: ScalingOptions,
218    ) -> Result<(), PicScaleError> {
219        let scaler =
220            Scaler::new(options.resampling_function).set_threading_policy(options.threading_policy);
221        let plan = AlphaPlanner::plan_generic_resize_with_alpha(
222            &scaler,
223            self.size(),
224            store.size(),
225            store.bit_depth,
226            options.premultiply_alpha,
227        )?;
228        plan.resample(self, store)
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235    use crate::{ImageStore, ResamplingFunction, ThreadingPolicy};
236    use core::f16;
237
238    #[test]
239    fn check_rgba_f16_resizing_vertical() {
240        let image_width = 8;
241        let image_height = 8;
242        const CN: usize = 4;
243        let mut image = vec![0_f16; image_height * image_width * CN];
244        for dst in image.as_chunks_mut::<CN>().0.iter_mut() {
245            dst[0] = (124f32 / 255f32) as f16;
246            dst[1] = (41f32 / 255f32) as f16;
247            dst[2] = (99f32 / 255f32) as f16;
248            dst[3] = 1f32 as f16;
249        }
250        let mut scaler = Scaler::new(ResamplingFunction::Lanczos3);
251        scaler.set_threading_policy(ThreadingPolicy::Single);
252        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
253        let mut target_store = ImageStoreMut::<f16, 4>::alloc(image_width, image_height / 2);
254        let planned = scaler
255            .plan_rgba_resampling_f16(src_store.size(), target_store.size(), false)
256            .unwrap();
257        planned.resample(&src_store, &mut target_store).unwrap();
258        let target_data = target_store.buffer.borrow();
259
260        for dst in target_data.chunks_exact(CN) {
261            assert!(
262                (dst[0] as f32 * 255f32 - 124f32).abs() < 3f32,
263                "R channel mismatch: {}",
264                dst[0] as f32 * 255f32
265            );
266            assert!(
267                (dst[1] as f32 * 255f32 - 41f32).abs() < 3f32,
268                "G channel mismatch: {}",
269                dst[1] as f32 * 255f32
270            );
271            assert!(
272                (dst[2] as f32 * 255f32 - 99f32).abs() < 3f32,
273                "B channel mismatch: {}",
274                dst[2] as f32 * 255f32
275            );
276            assert!(
277                (dst[3] as f32 - 1f32).abs() < 0.01f32,
278                "A channel mismatch: {}",
279                dst[3] as f32
280            );
281        }
282    }
283
284    #[test]
285    fn check_rgba_f16_resizing_vertical_threading() {
286        let image_width = 8;
287        let image_height = 8;
288        const CN: usize = 4;
289        let mut image = vec![0_f16; image_height * image_width * CN];
290        for dst in image.as_chunks_mut::<CN>().0.iter_mut() {
291            dst[0] = (124f32 / 255f32) as f16;
292            dst[1] = (41f32 / 255f32) as f16;
293            dst[2] = (99f32 / 255f32) as f16;
294            dst[3] = 1f32 as f16;
295        }
296        let scaler = Scaler::new(ResamplingFunction::Lanczos3)
297            .set_threading_policy(ThreadingPolicy::Adaptive);
298        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
299        let mut target_store = ImageStoreMut::<f16, 4>::alloc(image_width, image_height / 2);
300        let planned = scaler
301            .plan_rgba_resampling_f16(src_store.size(), target_store.size(), false)
302            .unwrap();
303        planned.resample(&src_store, &mut target_store).unwrap();
304        let target_data = target_store.buffer.borrow();
305
306        for dst in target_data.chunks_exact(CN) {
307            assert!(
308                (dst[0] as f32 * 255f32 - 124f32).abs() < 3f32,
309                "R channel mismatch: {}",
310                dst[0] as f32 * 255f32
311            );
312            assert!(
313                (dst[1] as f32 * 255f32 - 41f32).abs() < 3f32,
314                "G channel mismatch: {}",
315                dst[1] as f32 * 255f32
316            );
317            assert!(
318                (dst[2] as f32 * 255f32 - 99f32).abs() < 3f32,
319                "B channel mismatch: {}",
320                dst[2] as f32 * 255f32
321            );
322            assert!(
323                (dst[3] as f32 - 1f32).abs() < 0.01f32,
324                "A channel mismatch: {}",
325                dst[3] as f32
326            );
327        }
328    }
329
330    #[test]
331    fn check_rgba_f16_nearest_vertical() {
332        let image_width = 255;
333        let image_height = 512;
334        const CN: usize = 4;
335        let mut image = vec![0_f16; image_height * image_width * CN];
336        for dst in image.as_chunks_mut::<CN>().0.iter_mut() {
337            dst[0] = (124f32 / 255f32) as f16;
338            dst[1] = (41f32 / 255f32) as f16;
339            dst[2] = (99f32 / 255f32) as f16;
340            dst[3] = (77f32 / 255f32) as f16;
341        }
342        let mut scaler = Scaler::new(ResamplingFunction::Nearest);
343        scaler.set_threading_policy(ThreadingPolicy::Single);
344        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
345        let mut target_store = ImageStoreMut::<f16, 4>::alloc(image_width, image_height / 2);
346        let planned = scaler
347            .plan_rgba_resampling_f16(src_store.size(), target_store.size(), false)
348            .unwrap();
349        planned.resample(&src_store, &mut target_store).unwrap();
350        let target_data = target_store.buffer.borrow();
351
352        for dst in target_data.chunks_exact(CN) {
353            assert!(
354                (dst[0] as f32 * 255f32 - 124f32).abs() < 3f32,
355                "R channel mismatch: {}",
356                dst[0] as f32 * 255f32
357            );
358            assert!(
359                (dst[1] as f32 * 255f32 - 41f32).abs() < 3f32,
360                "G channel mismatch: {}",
361                dst[1] as f32 * 255f32
362            );
363            assert!(
364                (dst[2] as f32 * 255f32 - 99f32).abs() < 3f32,
365                "B channel mismatch: {}",
366                dst[2] as f32 * 255f32
367            );
368            assert!(
369                (dst[3] as f32 * 255f32 - 77f32).abs() < 3f32,
370                "A channel mismatch: {}",
371                dst[3] as f32 * 255f32
372            );
373        }
374    }
375
376    #[test]
377    fn check_rgba_f16_nearest_vertical_threading() {
378        let image_width = 255;
379        let image_height = 512;
380        const CN: usize = 4;
381        let mut image = vec![0_f16; image_height * image_width * CN];
382        for dst in image.as_chunks_mut::<CN>().0.iter_mut() {
383            dst[0] = (124f32 / 255f32) as f16;
384            dst[1] = (41f32 / 255f32) as f16;
385            dst[2] = (99f32 / 255f32) as f16;
386            dst[3] = (77f32 / 255f32) as f16;
387        }
388        let scaler = Scaler::new(ResamplingFunction::Nearest)
389            .set_threading_policy(ThreadingPolicy::Adaptive);
390        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
391        let mut target_store = ImageStoreMut::<f16, 4>::alloc(image_width, image_height / 2);
392        let planned = scaler
393            .plan_rgba_resampling_f16(src_store.size(), target_store.size(), false)
394            .unwrap();
395        planned.resample(&src_store, &mut target_store).unwrap();
396        let target_data = target_store.buffer.borrow();
397
398        for dst in target_data.chunks_exact(CN) {
399            assert!(
400                (dst[0] as f32 * 255f32 - 124f32).abs() < 3f32,
401                "R channel mismatch: {}",
402                dst[0] as f32 * 255f32
403            );
404            assert!(
405                (dst[1] as f32 * 255f32 - 41f32).abs() < 3f32,
406                "G channel mismatch: {}",
407                dst[1] as f32 * 255f32
408            );
409            assert!(
410                (dst[2] as f32 * 255f32 - 99f32).abs() < 3f32,
411                "B channel mismatch: {}",
412                dst[2] as f32 * 255f32
413            );
414            assert!(
415                (dst[3] as f32 * 255f32 - 77f32).abs() < 3f32,
416                "A channel mismatch: {}",
417                dst[3] as f32 * 255f32
418            );
419        }
420    }
421
422    #[test]
423    fn check_rgb_f16_resizing_vertical() {
424        let image_width = 8;
425        let image_height = 8;
426        const CN: usize = 3;
427        let mut image = vec![0_f16; image_height * image_width * CN];
428        for dst in image.as_chunks_mut::<CN>().0.iter_mut() {
429            dst[0] = (124f32 / 255f32) as f16;
430            dst[1] = (41f32 / 255f32) as f16;
431            dst[2] = (99f32 / 255f32) as f16;
432        }
433        let mut scaler = Scaler::new(ResamplingFunction::Lanczos3);
434        scaler.set_threading_policy(ThreadingPolicy::Single);
435        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
436        let mut target_store = ImageStoreMut::<f16, 3>::alloc(image_width, image_height / 2);
437        let planned = scaler
438            .plan_rgb_resampling_f16(src_store.size(), target_store.size())
439            .unwrap();
440        planned.resample(&src_store, &mut target_store).unwrap();
441        let target_data = target_store.buffer.borrow();
442
443        for dst in target_data.as_chunks::<CN>().0.iter() {
444            assert!(
445                (dst[0] as f32 * 255f32 - 124f32).abs() < 3f32,
446                "R channel mismatch: {}",
447                dst[0] as f32 * 255f32
448            );
449            assert!(
450                (dst[1] as f32 * 255f32 - 41f32).abs() < 3f32,
451                "G channel mismatch: {}",
452                dst[1] as f32 * 255f32
453            );
454            assert!(
455                (dst[2] as f32 * 255f32 - 99f32).abs() < 3f32,
456                "B channel mismatch: {}",
457                dst[2] as f32 * 255f32
458            );
459        }
460    }
461
462    #[test]
463    fn check_cbcr_f16_resizing_vertical() {
464        let image_width = 8;
465        let image_height = 8;
466        const CN: usize = 2;
467        let mut image = vec![0_f16; image_height * image_width * CN];
468        for dst in image.as_chunks_mut::<CN>().0.iter_mut() {
469            dst[0] = (124f32 / 255f32) as f16;
470            dst[1] = (41f32 / 255f32) as f16;
471        }
472        let mut scaler = Scaler::new(ResamplingFunction::Lanczos3);
473        scaler.set_threading_policy(ThreadingPolicy::Single);
474        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
475        let mut target_store = ImageStoreMut::<f16, 2>::alloc(image_width, image_height / 2);
476        let planned = scaler
477            .plan_cbcr_resampling_f16(src_store.size(), target_store.size())
478            .unwrap();
479        planned.resample(&src_store, &mut target_store).unwrap();
480        let target_data = target_store.buffer.borrow();
481
482        for dst in target_data.as_chunks::<CN>().0.iter() {
483            assert!(
484                (dst[0] as f32 * 255f32 - 124f32).abs() < 3f32,
485                "R channel mismatch: {}",
486                dst[0] as f32 * 255f32
487            );
488            assert!(
489                (dst[1] as f32 * 255f32 - 41f32).abs() < 3f32,
490                "G channel mismatch: {}",
491                dst[1] as f32 * 255f32
492            );
493        }
494    }
495
496    #[test]
497    fn check_planar_f16_resizing_vertical() {
498        let image_width = 8;
499        let image_height = 8;
500        const CN: usize = 1;
501        let mut image = vec![0_f16; image_height * image_width * CN];
502        for dst in image.as_chunks_mut::<CN>().0.iter_mut() {
503            dst[0] = (124f32 / 255f32) as f16;
504        }
505        let mut scaler = Scaler::new(ResamplingFunction::Lanczos3);
506        scaler.set_threading_policy(ThreadingPolicy::Single);
507        let src_store = ImageStore::from_slice(&image, image_width, image_height).unwrap();
508        let mut target_store = ImageStoreMut::<f16, 1>::alloc(image_width, image_height / 2);
509        let planned = scaler
510            .plan_planar_resampling_f16(src_store.size(), target_store.size())
511            .unwrap();
512        planned.resample(&src_store, &mut target_store).unwrap();
513        let target_data = target_store.buffer.borrow();
514
515        for dst in target_data.as_chunks::<CN>().0.iter() {
516            assert!(
517                (dst[0] as f32 * 255f32 - 124f32).abs() < 3f32,
518                "R channel mismatch: {}",
519                dst[0] as f32 * 255f32
520            );
521        }
522    }
523}