hyperion/instance/
black_border_detector.rs

1use crate::{image::Image, models};
2
3#[derive(Debug, Clone, Copy, PartialEq)]
4pub struct BlackBorder {
5    pub unknown: bool,
6    pub horizontal_size: u16,
7    pub vertical_size: u16,
8    threshold: u8,
9}
10
11impl BlackBorder {
12    pub fn new(threshold: u8) -> Self {
13        Self {
14            unknown: true,
15            horizontal_size: 0,
16            vertical_size: 0,
17            threshold,
18        }
19    }
20
21    fn is_black(&self, color: models::Color) -> bool {
22        color.red < self.threshold && color.green < self.threshold && color.blue < self.threshold
23    }
24
25    fn update(&mut self, xy: (Option<u16>, Option<u16>)) {
26        if let (Some(x), Some(y)) = xy {
27            self.unknown = false;
28            self.horizontal_size = y;
29            self.vertical_size = x;
30        } else {
31            self.unknown = true;
32            self.horizontal_size = 0;
33            self.vertical_size = 0;
34        }
35    }
36
37    fn process_default(&mut self, image: &impl Image) {
38        let width = image.width();
39        let height = image.height();
40        let width33 = width / 3;
41        let height33 = height / 3;
42        let width66 = width33 * 2;
43        let height66 = height33 * 2;
44        let x_center = width / 2;
45        let y_center = height / 2;
46
47        let width = width - 1;
48        let height = height - 1;
49
50        // Safety: width33 < width && height33 < height so x and y are in range
51        unsafe {
52            let first_non_black_x = (0..width33).find(|x| {
53                !self.is_black(image.color_at_unchecked(width - *x, y_center))
54                    || !self.is_black(image.color_at_unchecked(*x, height33))
55                    || !self.is_black(image.color_at_unchecked(*x, height66))
56            });
57
58            let first_non_black_y = (0..height33).find(|y| {
59                !self.is_black(image.color_at_unchecked(x_center, height - *y))
60                    || !self.is_black(image.color_at_unchecked(width33, *y))
61                    || !self.is_black(image.color_at_unchecked(width66, *y))
62            });
63
64            self.update((first_non_black_x, first_non_black_y));
65        }
66    }
67
68    fn process_classic(&mut self, image: &impl Image) {
69        let width = image.width() / 3;
70        let height = image.height() / 3;
71        let max_size = width.max(height);
72
73        let mut first_non_black_x = -1i32;
74        let mut first_non_black_y = -1i32;
75
76        for i in 0..max_size {
77            let x = i.min(width);
78            let y = i.min(height);
79
80            // Safety: x and y are in range, since width < image.width()
81            unsafe {
82                if !self.is_black(image.color_at_unchecked(x, y)) {
83                    first_non_black_x = x as _;
84                    first_non_black_y = y as _;
85                }
86            }
87        }
88
89        while first_non_black_x > 0 {
90            // Safety: first_non_black_x > 0 && first_non_black_x <= width
91            unsafe {
92                if first_non_black_y < 0
93                    || self.is_black(
94                        image.color_at_unchecked(
95                            (first_non_black_x - 1) as _,
96                            first_non_black_y as _,
97                        ),
98                    )
99                {
100                    break;
101                }
102            }
103
104            first_non_black_x -= 1;
105        }
106
107        while first_non_black_y > 0 {
108            // Safety: first_non_black_x >= 0 && first_non_black_y > 0
109            unsafe {
110                if self.is_black(
111                    image.color_at_unchecked(first_non_black_x as _, (first_non_black_y - 1) as _),
112                ) {
113                    break;
114                }
115            }
116
117            first_non_black_y -= 1;
118        }
119
120        self.update((
121            if first_non_black_x < 0 {
122                None
123            } else {
124                Some(first_non_black_x as _)
125            },
126            if first_non_black_y < 0 {
127                None
128            } else {
129                Some(first_non_black_y as _)
130            },
131        ));
132    }
133
134    fn process_osd(&mut self, image: &impl Image) {
135        let width = image.width();
136        let height = image.height();
137        let width33 = width / 3;
138        let height33 = height / 3;
139        let height66 = height33 * 2;
140        let y_center = height / 2;
141
142        let width = width - 1;
143        let height = height - 1;
144
145        // Safety: all operations are in range of the image dimensions
146        unsafe {
147            let first_non_black_x = (0..width33).find(|x| {
148                !self.is_black(image.color_at_unchecked(width - *x, y_center))
149                    || !self.is_black(image.color_at_unchecked(*x, height33))
150                    || !self.is_black(image.color_at_unchecked(*x, height66))
151            });
152
153            let x = first_non_black_x.unwrap_or(width33);
154
155            let first_non_black_y = (0..height33).find(|y| {
156                !self.is_black(image.color_at_unchecked(x, *y))
157                    || !self.is_black(image.color_at_unchecked(x, height - *y))
158                    || !self.is_black(image.color_at_unchecked(width - x, *y))
159                    || !self.is_black(image.color_at_unchecked(width - x, height - *y))
160            });
161
162            self.update((first_non_black_x, first_non_black_y));
163        }
164    }
165
166    fn process_letterbox(&mut self, image: &impl Image) {
167        let width = image.width();
168        let height = image.height();
169        let height33 = height / 3;
170        let width25 = width / 4;
171        let width75 = width25 * 3;
172        let x_center = width / 2;
173
174        let height = height - 1;
175
176        // Safety: all operations are in range of the image dimensions
177        unsafe {
178            let first_non_black_y = (0..height33).find(|y| {
179                !self.is_black(image.color_at_unchecked(x_center, *y))
180                    || !self.is_black(image.color_at_unchecked(width25, *y))
181                    || !self.is_black(image.color_at_unchecked(width75, *y))
182                    || !self.is_black(image.color_at_unchecked(width25, height - *y))
183                    || !self.is_black(image.color_at_unchecked(width75, height - *y))
184            });
185
186            self.update((
187                first_non_black_y,
188                if first_non_black_y.is_none() {
189                    None
190                } else {
191                    Some(0)
192                },
193            ));
194        }
195    }
196
197    pub fn process(&mut self, image: &impl Image, mode: models::BlackBorderDetectorMode) {
198        match mode {
199            models::BlackBorderDetectorMode::Default => self.process_default(image),
200            models::BlackBorderDetectorMode::Classic => self.process_classic(image),
201            models::BlackBorderDetectorMode::Osd => self.process_osd(image),
202            models::BlackBorderDetectorMode::Letterbox => self.process_letterbox(image),
203        }
204    }
205
206    pub fn blur(&mut self, blur: u16) {
207        if self.horizontal_size > 0 {
208            self.horizontal_size += blur;
209        }
210
211        if self.vertical_size > 0 {
212            self.vertical_size += blur;
213        }
214    }
215
216    pub fn get_ranges(
217        &self,
218        width: u16,
219        height: u16,
220    ) -> (std::ops::Range<u16>, std::ops::Range<u16>) {
221        if self.unknown {
222            (0..width, 0..height)
223        } else {
224            (
225                self.vertical_size.min(width / 2)..(width - self.vertical_size).max(width / 2),
226                self.horizontal_size.min(height / 2)
227                    ..(height - self.horizontal_size).max(height / 2),
228            )
229        }
230    }
231}
232
233impl Default for BlackBorder {
234    fn default() -> Self {
235        Self {
236            unknown: true,
237            horizontal_size: 0,
238            vertical_size: 0,
239            threshold: 0,
240        }
241    }
242}
243
244pub struct BlackBorderDetector {
245    config: models::BlackBorderDetector,
246    current_border: BlackBorder,
247    previous_border: BlackBorder,
248    consistent_cnt: u32,
249    inconsistent_cnt: u32,
250}
251
252impl BlackBorderDetector {
253    pub fn new(config: models::BlackBorderDetector) -> Self {
254        Self {
255            config,
256            current_border: Default::default(),
257            previous_border: Default::default(),
258            consistent_cnt: 0,
259            inconsistent_cnt: 0,
260        }
261    }
262
263    fn threshold(&self) -> u8 {
264        (self.config.threshold * 255 / 100).min(255) as u8
265    }
266
267    fn update_border(&mut self, new_border: BlackBorder) -> bool {
268        if new_border == self.previous_border {
269            self.consistent_cnt += 1;
270            self.inconsistent_cnt = 0;
271        } else {
272            self.inconsistent_cnt += 1;
273
274            if self.inconsistent_cnt <= self.config.max_inconsistent_cnt {
275                return false;
276            }
277
278            self.previous_border = new_border;
279            self.consistent_cnt = 0;
280        }
281
282        if self.current_border == new_border {
283            self.inconsistent_cnt = 0;
284            return false;
285        }
286
287        if new_border.unknown {
288            if self.consistent_cnt == self.config.unknown_frame_cnt {
289                self.current_border = new_border;
290                return true;
291            }
292        } else if self.current_border.unknown || self.consistent_cnt == self.config.border_frame_cnt
293        {
294            self.current_border = new_border;
295            return true;
296        }
297
298        false
299    }
300
301    pub fn current_border(&self) -> BlackBorder {
302        self.current_border
303    }
304
305    /// Process the given image
306    ///
307    /// # Returns
308    ///
309    /// true if a different border was detected, false otherwise
310    pub fn process(&mut self, image: &impl Image) -> bool {
311        let mut image_border = BlackBorder::new(self.threshold());
312
313        if !self.config.enable {
314            return self.update_border(image_border);
315        }
316
317        image_border.process(image, self.config.mode);
318        image_border.blur(self.config.blur_remove_cnt);
319
320        self.update_border(image_border)
321    }
322}