Skip to main content

falcon_ffmpeg/
frame.rs

1//! Decoded pictures, shared by both of falcon's video back ends.
2
3/// A decoded picture: `width * height` RGBA pixels, rows top to bottom,
4/// fully opaque.
5#[derive(Clone, Debug, PartialEq)]
6pub struct Frame {
7    pub width: usize,
8    pub height: usize,
9    pub rgba: Vec<u8>,
10}
11
12impl Frame {
13    /// The RGBA of the pixel at `(x, y)`.
14    pub fn pixel(&self, x: usize, y: usize) -> [u8; 4] {
15        let i = (y * self.width + x) * 4;
16        [self.rgba[i], self.rgba[i + 1], self.rgba[i + 2], self.rgba[i + 3]]
17    }
18
19    /// The average RGB over the whole picture.
20    pub fn mean(&self) -> [f32; 3] {
21        let n = (self.width * self.height).max(1) as f32;
22        let mut sum = [0.0f32; 3];
23        for px in self.rgba.as_chunks::<4>().0 {
24            for c in 0..3 {
25                sum[c] += px[c] as f32;
26            }
27        }
28        sum.map(|s| s / n)
29    }
30
31    /// Whether the picture is (nearly) black all over: a fade-in or a
32    /// title card's gap, not a picture worth using as a thumbnail.
33    pub fn is_black(&self) -> bool {
34        let [r, g, b] = self.mean();
35        if r.max(g).max(b) > BLACK_MEAN {
36            return false;
37        }
38        // A dark frame with something bright in it (stars, a logo) counts.
39        let bright = self.rgba.as_chunks::<4>().0.iter().filter(|p| p[0].max(p[1]).max(p[2]) > BLACK_PIXEL).count();
40        bright * 100 < self.width * self.height
41    }
42
43    /// The picture turned `turns` clockwise quarter turns.
44    pub fn turned(self, turns: u8) -> Frame {
45        let (w, h) = (self.width, self.height);
46        if turns.is_multiple_of(4) || w == 0 || h == 0 {
47            return self;
48        }
49        let (ow, oh) = if turns % 2 == 1 { (h, w) } else { (w, h) };
50        let mut rgba = vec![0u8; ow * oh * 4];
51        for oy in 0..oh {
52            for ox in 0..ow {
53                let (sx, sy) = match turns % 4 {
54                    1 => (oy, h - 1 - ox),
55                    2 => (w - 1 - ox, h - 1 - oy),
56                    _ => (w - 1 - oy, ox),
57                };
58                let (s, d) = ((sy * w + sx) * 4, (oy * ow + ox) * 4);
59                rgba[d..d + 4].copy_from_slice(&self.rgba[s..s + 4]);
60            }
61        }
62        Frame { width: ow, height: oh, rgba }
63    }
64}
65
66/// Mean channel level (0 to 255) at or under which a picture may be black.
67const BLACK_MEAN: f32 = 12.0;
68/// A pixel brighter than this is not black.
69const BLACK_PIXEL: u8 = 40;
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    fn solid(w: usize, h: usize, v: u8) -> Frame {
76        Frame { width: w, height: h, rgba: [v, v, v, 255].repeat(w * h) }
77    }
78
79    #[test]
80    fn black_frames_are_noticed() {
81        assert!(solid(8, 8, 0).is_black());
82        assert!(solid(8, 8, 10).is_black());
83        assert!(!solid(8, 8, 60).is_black());
84        // A few bright pixels in the dark are a picture.
85        let mut stars = solid(10, 10, 0);
86        for i in [0, 9, 44, 77, 98] {
87            stars.rgba[i * 4..i * 4 + 3].copy_from_slice(&[255, 255, 255]);
88        }
89        assert!(!stars.is_black());
90    }
91
92    #[test]
93    fn turns_rotate_clockwise() {
94        // 0 1 2   once:  3 0
95        // 3 4 5          4 1
96        //                5 2
97        let f = Frame { width: 3, height: 2, rgba: (0..6u8).flat_map(|n| [n, 0, 0, 255]).collect() };
98        let reds = |f: &Frame| f.rgba.as_chunks::<4>().0.iter().map(|p| p[0]).collect::<Vec<_>>();
99        let once = f.clone().turned(1);
100        assert_eq!((once.width, once.height), (2, 3));
101        assert_eq!(reds(&once), vec![3, 0, 4, 1, 5, 2]);
102        assert_eq!(reds(&f.clone().turned(2)), vec![5, 4, 3, 2, 1, 0]);
103        assert_eq!(reds(&f.clone().turned(3)), vec![2, 5, 1, 4, 0, 3]);
104        assert_eq!(f.clone().turned(4), f);
105    }
106}