1use crate::raw::preprocessor::BrightArea;
2use crate::raw::{
3 bounding_box::BoundingBox, colors::detect_color, colors::Color,
4 preprocessor::extract_bright_areas, utils::bbox_resize,
5};
6use image::{imageops::FilterType, DynamicImage, GenericImageView, ImageBuffer, Luma};
7
8#[derive(Debug, Clone)]
9pub struct Led {
10 pub bbox: BoundingBox,
11 pub color: Color,
12}
13
14#[derive(Debug)]
15pub struct LedDetectionConfig {
16 pub width: u32,
17 pub height: u32,
18 pub filter: FilterType,
19 pub radius_1: f32,
20 pub radius_2: f32,
21 pub threshold_value: u8,
22 pub min_size: (u32, u32),
23 pub max_size: (u32, u32),
24}
25
26impl Default for LedDetectionConfig {
27 fn default() -> Self {
28 Self {
29 width: 800,
31 height: 800,
32 filter: FilterType::Gaussian,
33 radius_1: 4.0,
34 radius_2: 8.0,
35 threshold_value: 10,
36 min_size: (10, 10),
37 max_size: (40, 40),
38 }
39 }
40}
41fn find_leds_areas(img: &ImageBuffer<Luma<u8>, Vec<u8>>) -> eyre::Result<Vec<BoundingBox>> {
42 let mut visited: Vec<Vec<bool>> =
43 vec![vec![false; img.height() as usize]; img.width() as usize];
44 let mut bounding_boxes = Vec::new();
45
46 for (x, y, pixel) in img.enumerate_pixels() {
47 if pixel[0] == 0 && !visited[x as usize][y as usize] {
48 let mut stack = vec![(x, y)];
50 let mut bbox = BoundingBox::new(x, y, x, y)?;
51
52 while let Some((cx, cy)) = stack.pop() {
53 if cx < img.width()
54 && cy < img.height()
55 && img.get_pixel(cx, cy)[0] == 0
56 && !visited[cx as usize][cy as usize]
57 {
58 let x_min = bbox.x_min().min(cx);
60 let y_min = bbox.y_min().min(cy);
61 let x_max = bbox.x_max().max(cx);
62 let y_max = bbox.y_max().max(cy);
63 bbox.set_coordinates(x_min, y_min, x_max, y_max)?;
64
65 if cx.saturating_sub(1) > 0 {
67 stack.push((cx - 1, cy));
68 } if cx + 1 < img.width() {
70 stack.push((cx + 1, cy));
71 } if cy.saturating_sub(1) > 0 {
73 stack.push((cx, cy - 1));
74 } if cy + 1 < img.height() {
76 stack.push((cx, cy + 1));
77 } visited[cx as usize][cy as usize] = true;
79 }
80 }
81
82 bounding_boxes.push(bbox);
83 }
84 }
85 Ok(bounding_boxes)
86}
87
88pub fn get_leds(image: &DynamicImage, config: &LedDetectionConfig) -> eyre::Result<Vec<Led>> {
89 let BrightArea {
90 thresholded,
91 resized,
92 } = extract_bright_areas(image, config)?;
93
94 let bounding_boxes = find_leds_areas(&thresholded)?;
96
97 let bounding_boxes_with_color: Vec<Led> = bounding_boxes
99 .clone()
100 .into_iter()
101 .filter_map(|bbox| {
102 if !bbox.is_within_size_bounds(config.min_size, config.max_size) {
103 None
104 } else {
105 let color = detect_color(&resized, &bbox);
106 if let Ok(bbox_on_original_image) =
107 bbox_resize(&bbox, &image.dimensions(), &resized.dimensions())
108 {
109 Some(Ok(Led {
110 bbox: bbox_on_original_image,
111 color,
112 }))
113 } else {
114 None
115 }
116 }
117 })
118 .collect::<eyre::Result<Vec<Led>>>()?;
119 Ok(bounding_boxes_with_color)
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use crate::distance;
126 use crate::raw::{bounding_box, utils::draw_bounding_boxes, BLUE, GREEN, RED, WHITE};
127 use image::{imageops::FilterType, Rgba};
128 use std::env;
129 use std::path::{Path, PathBuf};
130
131 fn create_new_path(path: &Path, suffix: &str) -> PathBuf {
132 let file_stem = path.file_stem().unwrap().to_str().unwrap();
133 let ext = path.extension().unwrap().to_str().unwrap();
134 let new_path = format!("{}_{}.{}", file_stem, suffix, ext);
135 path.with_file_name(new_path)
136 }
137
138 fn test_image_overlay(path: PathBuf, config: LedDetectionConfig) -> Vec<Led> {
139 let mut img = image::open(path.clone()).unwrap();
140
141 let mut red_boxes = vec![];
142 let mut blue_boxes = vec![];
143 let mut green_boxes = vec![];
144 let mut white_boxes = vec![];
145
146 let leds = get_leds(&img, &config).unwrap();
147
148 leds.iter().for_each(|led| match led.color {
149 Color::Red => red_boxes.push(led.bbox),
150 Color::Green => green_boxes.push(led.bbox),
151 Color::Blue => blue_boxes.push(led.bbox),
152 Color::White => white_boxes.push(led.bbox),
153 Color::Unknown => {}
154 });
155
156 draw_bounding_boxes(&mut img, red_boxes, Rgba(RED));
158 draw_bounding_boxes(&mut img, blue_boxes, Rgba(BLUE));
159 draw_bounding_boxes(&mut img, green_boxes, Rgba(GREEN));
160 draw_bounding_boxes(&mut img, white_boxes, Rgba(WHITE));
161 let overlay_color_path = create_new_path(&path, "overlay_color");
162 img.save(overlay_color_path).unwrap();
163
164 let bounding_boxes: Vec<BoundingBox> =
165 leds.clone().into_iter().map(|led| led.bbox).collect();
166
167 draw_bounding_boxes(&mut img, bounding_boxes, Rgba([255, 255, 255, 255]));
169 let overlay_path = create_new_path(&path, "overlay");
170 img.save(overlay_path).unwrap();
171
172 leds
173 }
174 #[test]
175 fn test_multiple_bulbs() {
176 let path = env::current_dir()
177 .unwrap()
178 .join("../resources/test_multiple_bulbs.png");
179 let config = LedDetectionConfig {
180 width: 400,
181 height: 400,
182 filter: FilterType::Gaussian,
183 radius_1: 4.0,
184 radius_2: 8.0,
185 threshold_value: 20,
186 min_size: (4, 4),
187 max_size: (40, 40),
188 };
189 let _ = test_image_overlay(path, config);
190 }
191
192 #[test]
193 fn test_with_enclosure() {
194 let path = env::current_dir()
195 .unwrap()
196 .join("../resources/test_with_enclosure.png");
197 let config = LedDetectionConfig {
198 width: 400,
199 height: 400,
200 filter: FilterType::Gaussian,
201 radius_1: 4.0,
202 radius_2: 8.0,
203 threshold_value: 20,
204 min_size: (6, 6),
205 max_size: (25, 25),
206 };
207 let leds = test_image_overlay(path, config);
208 let blue_leds: Vec<Led> = leds
209 .clone()
210 .into_iter()
211 .filter(|led| led.color == Color::Blue)
212 .collect();
213 assert_eq!(blue_leds.len(), 1);
214 let blue_led = &blue_leds[0];
215 assert_eq!(blue_led.bbox.x_min(), 520);
216 assert_eq!(blue_led.bbox.y_min(), 700);
217 assert_eq!(blue_led.bbox.x_max(), 558);
218 assert_eq!(blue_led.bbox.y_max(), 738);
219 }
220
221 #[test]
222 pub fn colours_higher_up() {
223 let path = env::current_dir()
224 .unwrap()
225 .join("../resources/new_leds/all_colours_higher_up.png");
226 let config = LedDetectionConfig {
227 width: 400,
228 height: 400,
229 filter: FilterType::Gaussian,
230 radius_1: 4.0,
231 radius_2: 8.0,
232 threshold_value: 12,
233 min_size: (3, 3),
234 max_size: (10, 10),
235 };
236 let leds = test_image_overlay(path, config);
237 }
238
239 #[test]
240 pub fn colours_higher_up_2() {
241 let path = env::current_dir()
242 .unwrap()
243 .join("../resources/new_leds/all_colours_higher_up_2.png");
244 let config = LedDetectionConfig {
245 width: 400,
246 height: 400,
247 filter: FilterType::Gaussian,
248 radius_1: 4.0,
249 radius_2: 15.0,
250 threshold_value: 10,
251 min_size: (3, 3),
252 max_size: (20, 20),
253 };
254 let leds = test_image_overlay(path, config);
255 }
256
257 #[test]
258 pub fn colours_horizontal() {
259 let path = env::current_dir()
260 .unwrap()
261 .join("../resources/new_leds/all_colours_horizontal.png");
262 let config = LedDetectionConfig {
263 width: 400,
264 height: 400,
265 filter: FilterType::Gaussian,
266 radius_1: 4.0,
267 radius_2: 15.0,
268 threshold_value: 10,
269 min_size: (5, 5),
270 max_size: (20, 20),
271 };
272 let leds = test_image_overlay(path, config);
273 }
274
275 #[test]
276 pub fn all_colours_vertical() {
277 let path = env::current_dir()
278 .unwrap()
279 .join("../resources/new_leds/all_colours_vertical.png");
280 let config = LedDetectionConfig {
281 width: 400,
282 height: 400,
283 filter: FilterType::Gaussian,
284 radius_1: 4.0,
285 radius_2: 15.0,
286 threshold_value: 10,
287 min_size: (5, 5),
288 max_size: (25, 25),
289 };
290 let leds = test_image_overlay(path, config);
291 }
292
293 #[test]
294 pub fn aerial_green() {
295 let path = env::current_dir()
296 .unwrap()
297 .join("../resources/new_leds/cropped_green_aerial.png");
298 let config = LedDetectionConfig {
299 width: 400,
300 height: 400,
301 filter: FilterType::Gaussian,
302 radius_1: 2.0,
303 radius_2: 8.0,
304 threshold_value: 12,
305 min_size: (3, 3),
306 max_size: (10, 10),
307 };
308 let leds = test_image_overlay(path, config);
309 assert_eq!(leds.len(), 1);
310 let led = &leds[0];
311 assert_eq!(led.color, Color::Green);
312 assert_eq!(led.bbox.x_min(), 1077);
313 assert_eq!(led.bbox.y_min(), 994);
314 assert_eq!(led.bbox.x_max(), 1106);
315 assert_eq!(led.bbox.y_max(), 1023);
316 }
317
318 #[test]
319 pub fn blue_800() {
320 let path = env::current_dir()
321 .unwrap()
322 .join("../resources/new_leds/blue_800.png");
323 let config = LedDetectionConfig {
324 width: 400,
325 height: 400,
326 filter: FilterType::Gaussian,
327 radius_1: 4.0,
328 radius_2: 8.0,
329 threshold_value: 15,
330 min_size: (10, 10),
331 max_size: (20, 20),
332 };
333 let leds = test_image_overlay(path, config);
334 assert_eq!(leds.len(), 1);
335 let led = &leds[0];
336 assert_eq!(led.bbox.x_min(), 1146);
337 assert_eq!(led.bbox.y_min(), 601);
338 assert_eq!(led.bbox.x_max(), 1213);
339 assert_eq!(led.bbox.y_max(), 672);
340 }
341
342 #[test]
343 pub fn blue_7000() {
344 let path = env::current_dir()
345 .unwrap()
346 .join("../resources/new_leds/blue_7000.png");
347 let config = LedDetectionConfig {
348 width: 400,
349 height: 400,
350 filter: FilterType::Gaussian,
351 radius_1: 4.0,
352 radius_2: 8.0,
353 threshold_value: 15,
354 min_size: (10, 10),
355 max_size: (40, 40),
356 };
357 let leds = test_image_overlay(path, config);
358 assert_eq!(leds.len(), 1);
359 let led = &leds[0];
360 assert_eq!(led.bbox.x_min(), 1175);
361 assert_eq!(led.bbox.y_min(), 186);
362 assert_eq!(led.bbox.x_max(), 1261);
363 assert_eq!(led.bbox.y_max(), 276);
364 }
365
366 #[test]
367 pub fn green_4000() {
368 let path = env::current_dir()
369 .unwrap()
370 .join("../resources/new_leds/green_4000.png");
371 let config = LedDetectionConfig {
372 width: 400,
373 height: 400,
374 filter: FilterType::Gaussian,
375 radius_1: 4.0,
376 radius_2: 8.0,
377 threshold_value: 10,
378 min_size: (20, 10),
379 max_size: (40, 40),
380 };
381 let leds = test_image_overlay(path, config);
382 assert_eq!(leds.len(), 1);
383 let green_led = &leds[0];
384 assert_eq!(green_led.color, Color::Green);
385 assert_eq!(green_led.bbox.x_min(), 991);
386 assert_eq!(green_led.bbox.y_min(), 331);
387 assert_eq!(green_led.bbox.x_max(), 1090);
388 assert_eq!(green_led.bbox.y_max(), 416);
389 }
390
391 #[test]
392 pub fn green_20000() {
393 let path = env::current_dir()
394 .unwrap()
395 .join("../resources/new_leds/green_20000.png");
396 let config = LedDetectionConfig {
397 width: 400,
398 height: 400,
399 filter: FilterType::Gaussian,
400 radius_1: 4.0,
401 radius_2: 8.0,
402 threshold_value: 15,
403 min_size: (10, 10),
404 max_size: (40, 40),
405 };
406 let leds = test_image_overlay(path, config);
407 assert_eq!(leds.len(), 1);
408 let green_led = &leds[0];
409 assert_eq!(green_led.color, Color::Green);
410 assert_eq!(green_led.bbox.x_min(), 1255);
411 assert_eq!(green_led.bbox.y_min(), 575);
412 assert_eq!(green_led.bbox.x_max(), 1326);
413 assert_eq!(green_led.bbox.y_max(), 660);
414 }
415
416 #[test]
417 pub fn red_from_drone() {
418 let path = env::current_dir()
419 .unwrap()
420 .join("../resources/from_drone/red.png");
421 let config = LedDetectionConfig {
422 width: 800,
423 height: 800,
424 filter: FilterType::Gaussian,
425 radius_1: 4.0,
426 radius_2: 8.0,
427 threshold_value: 10,
428 min_size: (7, 7),
429 max_size: (20, 20),
430 };
431 let leds = test_image_overlay(path, config);
432 assert_eq!(leds.len(), 1);
433 let led = &leds[0];
434 assert_eq!(led.color, Color::Red);
435 assert_eq!(led.bbox.x_min(), 1014);
436 assert_eq!(led.bbox.y_min(), 729);
437 assert_eq!(led.bbox.x_max(), 1039);
438 assert_eq!(led.bbox.y_max(), 752);
439 }
440
441 #[test]
442 pub fn blue_from_drone() {
443 let path = env::current_dir()
444 .unwrap()
445 .join("../resources/from_drone/blue.png");
446 let config = LedDetectionConfig {
447 width: 800,
448 height: 800,
449 filter: FilterType::Gaussian,
450 radius_1: 4.0,
451 radius_2: 8.0,
452 threshold_value: 10,
453 min_size: (7, 7),
454 max_size: (20, 20),
455 };
456 let leds = test_image_overlay(path, config);
457 assert_eq!(leds.len(), 1);
458 let led = &leds[0];
459 assert_eq!(led.color, Color::Blue);
460 assert_eq!(led.bbox.x_min(), 1010);
461 assert_eq!(led.bbox.y_min(), 646);
462 assert_eq!(led.bbox.x_max(), 1035);
463 assert_eq!(led.bbox.y_max(), 671);
464 }
465
466 #[test]
467 pub fn green_from_drone() {
468 let path = env::current_dir()
469 .unwrap()
470 .join("../resources/from_drone/green.png");
471 let config = LedDetectionConfig {
472 width: 800,
473 height: 800,
474 filter: FilterType::Gaussian,
475 radius_1: 4.0,
476 radius_2: 8.0,
477 threshold_value: 10,
478 min_size: (7, 7),
479 max_size: (20, 20),
480 };
481 let leds = test_image_overlay(path, config);
482 assert_eq!(leds.len(), 1);
483 let led = &leds[0];
484 assert_eq!(led.color, Color::Green);
485 assert_eq!(led.bbox.x_min(), 737);
486 assert_eq!(led.bbox.y_min(), 436);
487 assert_eq!(led.bbox.x_max(), 764);
488 assert_eq!(led.bbox.y_max(), 463);
489 }
490
491 #[test]
492 pub fn green_2_from_drone() {
493 let path = env::current_dir()
494 .unwrap()
495 .join("../resources/from_drone/green_2.png");
496 let config = LedDetectionConfig {
497 width: 800,
498 height: 800,
499 filter: FilterType::Gaussian,
500 radius_1: 4.0,
501 radius_2: 8.0,
502 threshold_value: 10,
503 min_size: (7, 7),
504 max_size: (20, 20),
505 };
506 let leds = test_image_overlay(path, config);
507 assert_eq!(leds.len(), 1);
508 let led = &leds[0];
509 assert_eq!(led.color, Color::Green);
510 assert_eq!(led.bbox.x_min(), 854);
511 assert_eq!(led.bbox.y_min(), 556);
512 assert_eq!(led.bbox.x_max(), 883);
513 assert_eq!(led.bbox.y_max(), 586);
514 }
515 #[test]
516 pub fn warm_white_from_drone() {
517 let path = env::current_dir()
518 .unwrap()
519 .join("../resources/from_drone/warm_white.png");
520 let config = LedDetectionConfig {
521 width: 800,
522 height: 800,
523 filter: FilterType::Gaussian,
524 radius_1: 4.0,
525 radius_2: 8.0,
526 threshold_value: 10,
527 min_size: (7, 7),
528 max_size: (20, 20),
529 };
530 let leds = test_image_overlay(path, config);
531 assert_eq!(leds.len(), 1);
532 let led = &leds[0];
533 assert_eq!(led.color, Color::White);
534 assert_eq!(led.bbox.x_min(), 966);
535 assert_eq!(led.bbox.y_min(), 694);
536 assert_eq!(led.bbox.x_max(), 995);
537 assert_eq!(led.bbox.y_max(), 723);
538 }
539
540 #[test]
541 pub fn warm_white_2_from_drone() {
542 let path = env::current_dir()
543 .unwrap()
544 .join("../resources/from_drone/warm_white_2.png");
545 let config = LedDetectionConfig {
546 width: 800,
547 height: 800,
548 filter: FilterType::Gaussian,
549 radius_1: 4.0,
550 radius_2: 8.0,
551 threshold_value: 10,
552 min_size: (7, 7),
553 max_size: (20, 20),
554 };
555 let leds = test_image_overlay(path, config);
556 assert_eq!(leds.len(), 1);
557 let led = &leds[0];
558 assert_eq!(led.color, Color::White);
559 assert_eq!(led.bbox.x_min(), 995);
560 assert_eq!(led.bbox.y_min(), 561);
561 assert_eq!(led.bbox.x_max(), 1023);
562 assert_eq!(led.bbox.y_max(), 588);
563 }
564
565 #[test]
566 pub fn both_white_from_drone() {
567 let path = env::current_dir()
568 .unwrap()
569 .join("../resources/from_drone/both_white.png");
570 let config = LedDetectionConfig {
571 width: 800,
572 height: 800,
573 filter: FilterType::Gaussian,
574 radius_1: 4.0,
575 radius_2: 8.0,
576 threshold_value: 10,
577 min_size: (7, 7),
578 max_size: (20, 20),
579 };
580 let leds = test_image_overlay(path, config);
581 assert_eq!(leds.len(), 2);
582 let led = &leds[0];
583 let led_2 = &leds[1];
584 assert_eq!(led.color, Color::White);
585 assert_eq!(led_2.color, Color::White);
586 }
587
588 #[test]
589 pub fn blue_green_red_from_drone() {
590 let path = env::current_dir()
591 .unwrap()
592 .join("../resources/from_drone/blue_green_red.png");
593 let config = LedDetectionConfig {
594 width: 800,
595 height: 800,
596 filter: FilterType::Gaussian,
597 radius_1: 4.0,
598 radius_2: 8.0,
599 threshold_value: 10,
600 min_size: (7, 7),
601 max_size: (20, 20),
602 };
603 let leds = test_image_overlay(path, config);
604 assert_eq!(leds.len(), 3);
605 let mut blue_led = leds[0].clone();
606 let mut red_led = leds[0].clone();
607 let mut green_led = leds[0].clone();
608 for led in leds {
609 match led.color {
610 Color::Red => red_led = led,
611 Color::Green => green_led = led,
612 Color::Blue => blue_led = led,
613 _ => {}
614 }
615 }
616
617 let b_g = distance(&blue_led, &green_led);
618 let g_r = distance(&green_led, &red_led);
619 let r_b = distance(&red_led, &blue_led);
620
621 assert_eq!(blue_led.bbox.x_min(), 625);
622 assert_eq!(blue_led.bbox.y_min(), 342);
623 assert_eq!(blue_led.bbox.x_max(), 650);
624 assert_eq!(blue_led.bbox.y_max(), 367);
625
626 assert_eq!(green_led.bbox.x_min(), 927);
627 assert_eq!(green_led.bbox.y_min(), 796);
628 assert_eq!(green_led.bbox.x_max(), 952);
629 assert_eq!(green_led.bbox.y_max(), 823);
630
631 assert_eq!(red_led.bbox.x_min(), 392);
632 assert_eq!(red_led.bbox.y_min(), 765);
633 assert_eq!(red_led.bbox.x_max(), 406);
634 assert_eq!(red_led.bbox.y_max(), 779);
635
636 assert_eq!(b_g, 546);
637 assert_eq!(r_b, 481);
638 assert_eq!(g_r, 541);
639 }
640
641 #[test]
642 pub fn green_blue_red_from_drone() {
643 let path = env::current_dir()
644 .unwrap()
645 .join("../resources/from_drone/green_blue_red.png");
646 let config = LedDetectionConfig {
647 width: 800,
648 height: 800,
649 filter: FilterType::Gaussian,
650 radius_1: 4.0,
651 radius_2: 8.0,
652 threshold_value: 10,
653 min_size: (7, 7),
654 max_size: (20, 20),
655 };
656 let leds = test_image_overlay(path, config);
657 assert_eq!(leds.len(), 3);
658 let mut blue_led = leds[0].clone();
659 let mut red_led = leds[0].clone();
660 let mut green_led = leds[0].clone();
661 for led in leds {
662 match led.color {
663 Color::Red => red_led = led,
664 Color::Green => green_led = led,
665 Color::Blue => blue_led = led,
666 _ => {}
667 }
668 }
669
670 let b_g = distance(&blue_led, &green_led);
671 let g_r = distance(&green_led, &red_led);
672 let r_b = distance(&red_led, &blue_led);
673
674 assert_eq!(blue_led.bbox.x_min(), 796);
675 assert_eq!(blue_led.bbox.y_min(), 392);
676 assert_eq!(blue_led.bbox.x_max(), 821);
677 assert_eq!(blue_led.bbox.y_max(), 417);
678
679 assert_eq!(green_led.bbox.x_min(), 1104);
680 assert_eq!(green_led.bbox.y_min(), 825);
681 assert_eq!(green_led.bbox.x_max(), 1129);
682 assert_eq!(green_led.bbox.y_max(), 850);
683
684 assert_eq!(red_led.bbox.x_min(), 571);
685 assert_eq!(red_led.bbox.y_min(), 819);
686 assert_eq!(red_led.bbox.x_max(), 589);
687 assert_eq!(red_led.bbox.y_max(), 838);
688
689 assert_eq!(b_g, 531);
690 assert_eq!(r_b, 481);
691 assert_eq!(g_r, 536);
692 }
693
694 #[test]
695 pub fn red_green_from_drone() {
696 let path = env::current_dir()
697 .unwrap()
698 .join("../resources/from_drone/red_green.png");
699 let config = LedDetectionConfig {
700 width: 800,
701 height: 800,
702 filter: FilterType::Gaussian,
703 radius_1: 4.0,
704 radius_2: 8.0,
705 threshold_value: 10,
706 min_size: (7, 7),
707 max_size: (20, 20),
708 };
709 let leds = test_image_overlay(path, config);
710 assert_eq!(leds.len(), 2);
711 let mut red_led = leds[0].clone();
712 let mut green_led = leds[0].clone();
713 for led in leds {
714 match led.color {
715 Color::Red => red_led = led,
716 Color::Green => green_led = led,
717 _ => {}
718 }
719 }
720 let g_r = distance(&green_led, &red_led);
721
722 assert_eq!(green_led.bbox.x_min(), 1131);
723 assert_eq!(green_led.bbox.y_min(), 423);
724 assert_eq!(green_led.bbox.x_max(), 1160);
725 assert_eq!(green_led.bbox.y_max(), 452);
726
727 assert_eq!(red_led.bbox.x_min(), 602);
728 assert_eq!(red_led.bbox.y_min(), 419);
729 assert_eq!(red_led.bbox.x_max(), 625);
730 assert_eq!(red_led.bbox.y_max(), 440);
731
732 assert_eq!(g_r, 532);
733 }
734}