hs_hackathon_vision/raw/
led_detector.rs

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            // According to last drone pics
30            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            // Found a new island, perform flood fill and calculate bounding box
49            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                    // Update bounding box
59                    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                    // Add neighboring pixels to the stack if they are within the image bounds
66                    if cx.saturating_sub(1) > 0 {
67                        stack.push((cx - 1, cy));
68                    } // left
69                    if cx + 1 < img.width() {
70                        stack.push((cx + 1, cy));
71                    } // right
72                    if cy.saturating_sub(1) > 0 {
73                        stack.push((cx, cy - 1));
74                    } // up
75                    if cy + 1 < img.height() {
76                        stack.push((cx, cy + 1));
77                    } // down
78                    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    // Use counting-islands algorithm to find islands of "very bright" pixels
95    let bounding_boxes = find_leds_areas(&thresholded)?;
96
97    // For each bounding box, detect color
98    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        // For debug purposes, overlay bounding box with corresponding color
157        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        // Overlay all bounding boxes onto original image
168        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}