hyperion/instance/
black_border_detector.rs1use 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 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 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 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 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 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 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 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}