core/scale: add options support
[nihav.git] / nihav-core / src / scale / mod.rs
index 13762bd01e4d65e16536fa2740004930411fd57f..6bf6e272273887dac36bad6147bd0147e34b88bc 100644 (file)
@@ -1,15 +1,41 @@
+//! Image conversion functionality.
+
+//! # Examples
+//!
+//! Convert input image into YUV one and scale down two times.
+//! ```no_run
+//! use nihav_core::scale::*;
+//! use nihav_core::formats::{RGB24_FORMAT, YUV420_FORMAT};
+//! use nihav_core::frame::{alloc_video_buffer, NAVideoInfo};
+//!
+//! let mut in_pic = alloc_video_buffer(NAVideoInfo::new(640, 480, false, RGB24_FORMAT), 4).unwrap();
+//! let mut out_pic = alloc_video_buffer(NAVideoInfo::new(320, 240, false, YUV420_FORMAT), 4).unwrap();
+//! let in_fmt = get_scale_fmt_from_pic(&in_pic);
+//! let out_fmt = get_scale_fmt_from_pic(&out_pic);
+//! let mut scaler = NAScale::new(in_fmt, out_fmt).unwrap();
+//! scaler.convert(&in_pic, &mut out_pic).unwrap();
+//! ```
 use crate::frame::*;
 
 mod kernel;
 
 mod colorcvt;
 mod repack;
+#[allow(clippy::module_inception)]
 mod scale;
 
+mod palette;
+
+pub use crate::scale::palette::{palettise_frame, QuantisationMode, PaletteSearchMode};
+
+/// Image format information used by the converter.
 #[derive(Clone,Copy,PartialEq)]
 pub struct ScaleInfo {
+    /// Pixel format description.
     pub fmt:    NAPixelFormaton,
+    /// Image width.
     pub width:  usize,
+    /// Image height.
     pub height: usize,
 }
 
@@ -19,16 +45,23 @@ impl std::fmt::Display for ScaleInfo {
     }
 }
 
+/// A list specifying general image conversion errors.
 #[derive(Debug,Clone,Copy,PartialEq)]
 #[allow(dead_code)]
 pub enum ScaleError {
+    /// Input or output buffer contains no image data.
     NoFrame,
+    /// Allocation failed.
     AllocError,
+    /// Invalid argument.
     InvalidArgument,
+    /// Feature is not implemented.
     NotImplemented,
+    /// Internal implementation bug.
     Bug,
 }
 
+/// A specialised `Result` type for image conversion operations.
 pub type ScaleResult<T> = Result<T, ScaleError>;
 
 /*trait Kernel {
@@ -56,6 +89,7 @@ const KERNELS: &[KernelDesc] = &[
     KernelDesc { name: "pack",          create: repack::create_pack },
     KernelDesc { name: "unpack",        create: repack::create_unpack },
     KernelDesc { name: "depal",         create: repack::create_depal },
+    KernelDesc { name: "palette",       create: palette::create_palettise },
     KernelDesc { name: "scale",         create: scale::create_scale },
     KernelDesc { name: "rgb_to_yuv",    create: colorcvt::create_rgb2yuv },
     KernelDesc { name: "yuv_to_rgb",    create: colorcvt::create_yuv2rgb },
@@ -68,15 +102,16 @@ struct Stage {
     worker:     Box<dyn kernel::Kernel>,
 }
 
+/// Converts input picture information into format used by scaler.
 pub fn get_scale_fmt_from_pic(pic: &NABufferType) -> ScaleInfo {
     let info = pic.get_video_info().unwrap();
     ScaleInfo { fmt: info.get_format(), width: info.get_width(), height: info.get_height() }
 }
 
 impl Stage {
-    fn new(name: &str, in_fmt: &ScaleInfo, dest_fmt: &ScaleInfo) -> ScaleResult<Self> {
+    fn new(name: &str, in_fmt: &ScaleInfo, dest_fmt: &ScaleInfo, options: &[(String, String)]) -> ScaleResult<Self> {
         let mut worker = KernelDesc::find(name)?;
-        let tmp_pic = worker.init(in_fmt, dest_fmt)?;
+        let tmp_pic = worker.init(in_fmt, dest_fmt, options)?;
         let fmt_out = get_scale_fmt_from_pic(&tmp_pic);
         Ok(Self { fmt_out, tmp_pic, next: None, worker })
     }
@@ -105,6 +140,7 @@ impl Stage {
     }
 }
 
+/// Image format converter.
 pub struct NAScale {
     fmt_in:         ScaleInfo,
     fmt_out:        ScaleInfo,
@@ -155,6 +191,39 @@ fn copy(pic_in: &NABufferType, pic_out: &mut NABufferType)
                 }
             }
         }
+    } else if let (Some(ref sbuf), Some(ref mut dbuf)) = (pic_in.get_vbuf16(), pic_out.get_vbuf16()) {
+        let mut same = true;
+        let num_components = sbuf.get_info().get_format().get_num_comp();
+        for i in 0..num_components {
+            if sbuf.get_stride(i) != dbuf.get_stride(i) {
+                same = false;
+                break;
+            }
+            if sbuf.get_offset(i) != dbuf.get_offset(i) {
+                same = false;
+                break;
+            }
+        }
+        if same {
+            let sdata = sbuf.get_data();
+            let ddata = dbuf.get_data_mut().unwrap();
+            ddata.copy_from_slice(&sdata[0..]);
+        } else {
+            let sdata = sbuf.get_data();
+            for comp in 0..num_components {
+                let (_, h) = sbuf.get_dimensions(comp);
+                let src = &sdata[sbuf.get_offset(comp)..];
+                let sstride = sbuf.get_stride(comp);
+                let doff = dbuf.get_offset(comp);
+                let dstride = dbuf.get_stride(comp);
+                let ddata = dbuf.get_data_mut().unwrap();
+                let dst = &mut ddata[doff..];
+                let copy_size = sstride.min(dstride);
+                for (dline, sline) in dst.chunks_exact_mut(dstride).take(h).zip(src.chunks_exact(sstride)) {
+                    (&mut dline[..copy_size]).copy_from_slice(&sline[..copy_size]);
+                }
+            }
+        }
     } else {
         unimplemented!();
     }
@@ -181,13 +250,32 @@ fn is_better_fmt(a: &ScaleInfo, b: &ScaleInfo) -> bool {
     }
     false
 }
-fn build_pipeline(ifmt: &ScaleInfo, ofmt: &ScaleInfo, just_convert: bool) -> ScaleResult<Option<Stage>> {
+fn fmt_needs_scale(ifmt: &NAPixelFormaton, ofmt: &NAPixelFormaton) -> bool {
+    for (ichr, ochr) in ifmt.comp_info.iter().zip(ofmt.comp_info.iter()) {
+        if let (Some(ic), Some(oc)) = (ichr, ochr) {
+            if ic.h_ss != oc.h_ss || ic.v_ss != oc.v_ss {
+                return true;
+            }
+        }
+    }
+    false
+}
+fn build_pipeline(ifmt: &ScaleInfo, ofmt: &ScaleInfo, just_convert: bool, options: &[(String, String)]) -> ScaleResult<Option<Stage>> {
+    let mut debug = false;
+    for (name, value) in options.iter() {
+        if name == "debug" && (value == "" || value == "true") {
+            debug = true;
+            break;
+        }
+    }
+
     let inname  = ifmt.fmt.get_model().get_short_name();
     let outname = ofmt.fmt.get_model().get_short_name();
 
-println!("convert {} -> {}", ifmt, ofmt);
-    let needs_scale = if (ofmt.fmt.get_max_subsampling() > 0) &&
-        (ofmt.fmt.get_max_subsampling() != ifmt.fmt.get_max_subsampling()) {
+    if debug {
+        println!("convert {} -> {}", ifmt, ofmt);
+    }
+    let needs_scale = if fmt_needs_scale(&ifmt.fmt, &ofmt.fmt) {
             true
         } else {
             !just_convert
@@ -197,47 +285,67 @@ println!("convert {} -> {}", ifmt, ofmt);
     let needs_convert = inname != outname;
     let scale_before_cvt = is_better_fmt(&ifmt, &ofmt) && needs_convert
                            && (ofmt.fmt.get_max_subsampling() == 0);
+    let needs_palettise = ofmt.fmt.palette;
 //todo stages for model and gamma conversion
 
     let mut stages: Option<Stage> = None;
     let mut cur_fmt = *ifmt;
 
     if needs_unpack {
-println!("[adding unpack]");
+        if debug {
+            println!("[adding unpack]");
+        }
         let new_stage = if !cur_fmt.fmt.is_paletted() {
-                Stage::new("unpack", &cur_fmt, &ofmt)?
+                Stage::new("unpack", &cur_fmt, &ofmt, options)?
             } else {
-                Stage::new("depal", &cur_fmt, &ofmt)?
+                Stage::new("depal", &cur_fmt, &ofmt, options)?
             };
         cur_fmt = new_stage.fmt_out;
         add_stage!(stages, new_stage);
     }
     if needs_scale && scale_before_cvt {
-println!("[adding scale]");
-        let new_stage = Stage::new("scale", &cur_fmt, &ofmt)?;
+        if debug {
+            println!("[adding scale]");
+        }
+        let new_stage = Stage::new("scale", &cur_fmt, &ofmt, options)?;
         cur_fmt = new_stage.fmt_out;
         add_stage!(stages, new_stage);
     }
     if needs_convert {
-println!("[adding convert]");
+        if debug {
+            println!("[adding convert]");
+        }
         let cvtname = format!("{}_to_{}", inname, outname);
-println!("[{}]", cvtname);
-        let new_stage = Stage::new(&cvtname, &cur_fmt, &ofmt)?;
+        if debug {
+            println!("[{}]", cvtname);
+        }
+        let new_stage = Stage::new(&cvtname, &cur_fmt, &ofmt, options)?;
 //todo if fails try converting via RGB or YUV
         cur_fmt = new_stage.fmt_out;
         add_stage!(stages, new_stage);
 //todo alpha plane copy/add
     }
     if needs_scale && !scale_before_cvt {
-println!("[adding scale]");
-        let new_stage = Stage::new("scale", &cur_fmt, &ofmt)?;
+        if debug {
+            println!("[adding scale]");
+        }
+        let new_stage = Stage::new("scale", &cur_fmt, &ofmt, options)?;
         cur_fmt = new_stage.fmt_out;
         add_stage!(stages, new_stage);
     }
-//todo flip if needed
-    if needs_pack {
-println!("[adding pack]");
-        let new_stage = Stage::new("pack", &cur_fmt, &ofmt)?;
+    if needs_pack && !needs_palettise {
+        if debug {
+            println!("[adding pack]");
+        }
+        let new_stage = Stage::new("pack", &cur_fmt, &ofmt, options)?;
+        //cur_fmt = new_stage.fmt_out;
+        add_stage!(stages, new_stage);
+    }
+    if needs_palettise {
+        if debug {
+            println!("[adding palettise]");
+        }
+        let new_stage = Stage::new("palette", &cur_fmt, &ofmt, options)?;
         //cur_fmt = new_stage.fmt_out;
         add_stage!(stages, new_stage);
     }
@@ -245,24 +353,118 @@ println!("[adding pack]");
     if let Some(ref mut head) = stages {
         head.drop_last_tmp();
     }
-    
+
     Ok(stages)
 }
 
+fn swap_plane<T:Copy>(data: &mut [T], stride: usize, h: usize, line0: &mut [T], line1: &mut [T]) {
+    let mut doff0 = 0;
+    let mut doff1 = stride * (h - 1);
+    for _ in 0..h/2 {
+        line0.copy_from_slice(&data[doff0..][..stride]);
+        line1.copy_from_slice(&data[doff1..][..stride]);
+        (&mut data[doff1..][..stride]).copy_from_slice(line0);
+        (&mut data[doff0..][..stride]).copy_from_slice(line1);
+        doff0 += stride;
+        doff1 -= stride;
+    }
+}
+
+/// Flips the picture contents.
+pub fn flip_picture(pic: &mut NABufferType) -> ScaleResult<()> {
+    match pic {
+        NABufferType::Video(ref mut vb) => {
+            let ncomp = vb.get_num_components();
+            for comp in 0..ncomp {
+                let off    = vb.get_offset(comp);
+                let stride = vb.get_stride(comp);
+                let (_, h) = vb.get_dimensions(comp);
+                let data = vb.get_data_mut().unwrap();
+                let mut line0 = vec![0; stride];
+                let mut line1 = vec![0; stride];
+                swap_plane(&mut data[off..], stride, h, line0.as_mut_slice(), line1.as_mut_slice());
+            }
+        },
+        NABufferType::Video16(ref mut vb) => {
+            let ncomp = vb.get_num_components().max(1);
+            for comp in 0..ncomp {
+                let off    = vb.get_offset(comp);
+                let stride = vb.get_stride(comp);
+                let (_, h) = vb.get_dimensions(comp);
+                let data = vb.get_data_mut().unwrap();
+                let mut line0 = vec![0; stride];
+                let mut line1 = vec![0; stride];
+                swap_plane(&mut data[off..], stride, h, line0.as_mut_slice(), line1.as_mut_slice());
+            }
+        },
+        NABufferType::Video32(ref mut vb) => {
+            let ncomp = vb.get_num_components().max(1);
+            for comp in 0..ncomp {
+                let off    = vb.get_offset(comp);
+                let stride = vb.get_stride(comp);
+                let (_, h) = vb.get_dimensions(comp);
+                let data = vb.get_data_mut().unwrap();
+                let mut line0 = vec![0; stride];
+                let mut line1 = vec![0; stride];
+                swap_plane(&mut data[off..], stride, h, line0.as_mut_slice(), line1.as_mut_slice());
+            }
+        },
+        NABufferType::VideoPacked(ref mut vb) => {
+            let ncomp = vb.get_num_components();
+            for comp in 0..ncomp {
+                let off    = vb.get_offset(comp);
+                let stride = vb.get_stride(comp);
+                let (_, h) = vb.get_dimensions(comp);
+                let data = vb.get_data_mut().unwrap();
+                let mut line0 = vec![0; stride];
+                let mut line1 = vec![0; stride];
+                swap_plane(&mut data[off..], stride, h, line0.as_mut_slice(), line1.as_mut_slice());
+            }
+            if ncomp == 0 && vb.get_stride(0) != 0 {
+                let off    = vb.get_offset(0);
+                let stride = vb.get_stride(0);
+                let (_, h) = vb.get_dimensions(0);
+                let data = vb.get_data_mut().unwrap();
+                let mut line0 = vec![0; stride];
+                let mut line1 = vec![0; stride];
+                swap_plane(&mut data[off..], stride, h, line0.as_mut_slice(), line1.as_mut_slice());
+            }
+        },
+        _ => { return Err(ScaleError::InvalidArgument); },
+    };
+    Ok(())
+}
+
 impl NAScale {
+    /// Constructs a new `NAScale` instance.
     pub fn new(fmt_in: ScaleInfo, fmt_out: ScaleInfo) -> ScaleResult<Self> {
         let pipeline;
         let just_convert = (fmt_in.width == fmt_out.width) && (fmt_in.height == fmt_out.height);
         if fmt_in != fmt_out {
-            pipeline = build_pipeline(&fmt_in, &fmt_out, just_convert)?;
+            pipeline = build_pipeline(&fmt_in, &fmt_out, just_convert, &[])?;
         } else {
             pipeline = None;
         }
         Ok(Self { fmt_in, fmt_out, just_convert, pipeline })
     }
+    /// Constructs a new `NAScale` instance taking into account provided options.
+    pub fn new_with_options(fmt_in: ScaleInfo, fmt_out: ScaleInfo, options: &[(String, String)]) -> ScaleResult<Self> {
+        let pipeline;
+        let just_convert = (fmt_in.width == fmt_out.width) && (fmt_in.height == fmt_out.height);
+        if fmt_in != fmt_out {
+            pipeline = build_pipeline(&fmt_in, &fmt_out, just_convert, options)?;
+        } else {
+            pipeline = None;
+        }
+        Ok(Self { fmt_in, fmt_out, just_convert, pipeline })
+    }
+    /// Checks whether requested conversion operation is needed at all.
     pub fn needs_processing(&self) -> bool { self.pipeline.is_some() }
+    /// Returns the input image format.
     pub fn get_in_fmt(&self) -> ScaleInfo { self.fmt_in }
+    /// Returns the output image format.
     pub fn get_out_fmt(&self) -> ScaleInfo { self.fmt_out }
+    /// Performs the image format conversion.
     pub fn convert(&mut self, pic_in: &NABufferType, pic_out: &mut NABufferType) -> ScaleResult<()> {
         let in_info  = pic_in.get_video_info();
         let out_info = pic_out.get_video_info();
@@ -273,14 +475,19 @@ impl NAScale {
                 (in_info.get_width() != out_info.get_width() || in_info.get_height() != out_info.get_height()) {
             return Err(ScaleError::InvalidArgument);
         }
+        let needs_flip = in_info.is_flipped() ^ out_info.is_flipped();
         check_format(in_info,  &self.fmt_in,  self.just_convert)?;
         check_format(out_info, &self.fmt_out, self.just_convert)?;
-        if let Some(ref mut pipe) = self.pipeline {
-            pipe.process(pic_in, pic_out)
-        } else {
-            copy(pic_in, pic_out);
-            Ok(())
+        let ret = if let Some(ref mut pipe) = self.pipeline {
+                pipe.process(pic_in, pic_out)
+            } else {
+                copy(pic_in, pic_out);
+                Ok(())
+            };
+        if ret.is_ok() && needs_flip {
+            flip_picture(pic_out)?;
         }
+        ret
     }
 }
 
@@ -367,8 +574,27 @@ mod test {
         let uoff = obuf.get_offset(1);
         let voff = obuf.get_offset(2);
         let odata = obuf.get_data();
-        assert_eq!(odata[yoff], 28);
-        assert_eq!(odata[uoff], 154);
-        assert_eq!(odata[voff], 103);
+        assert_eq!(odata[yoff], 11);
+        assert_eq!(odata[uoff], 162);
+        assert_eq!(odata[voff], 118);
+    }
+    #[test]
+    fn test_scale_and_convert_to_pal() {
+        let mut in_pic = alloc_video_buffer(NAVideoInfo::new(7, 3, false, YUV420_FORMAT), 3).unwrap();
+        fill_pic(&mut in_pic, 142);
+        let mut out_pic = alloc_video_buffer(NAVideoInfo::new(4, 4, false, PAL8_FORMAT), 0).unwrap();
+        fill_pic(&mut out_pic, 0);
+        let ifmt = get_scale_fmt_from_pic(&in_pic);
+        let ofmt = get_scale_fmt_from_pic(&out_pic);
+        let mut scaler = NAScale::new(ifmt, ofmt).unwrap();
+        scaler.convert(&in_pic, &mut out_pic).unwrap();
+        let obuf = out_pic.get_vbuf().unwrap();
+        let dataoff = obuf.get_offset(0);
+        let paloff  = obuf.get_offset(1);
+        let odata = obuf.get_data();
+        assert_eq!(odata[dataoff], 0);
+        assert_eq!(odata[paloff], 157);
+        assert_eq!(odata[paloff + 1], 129);
+        assert_eq!(odata[paloff + 2], 170);
     }
 }