feat(vida): add polygon mask to filter ground/obstacle false detections

Introduce configurable polygon masks that block ground, trees, and other
obstacles from triggering false meteor/fireball detections. Masks are
applied after thresholding but before morphology and line detection,
shared via Arc between both detectors. CLI supports --mask <json> flag.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
grabbit 2026-02-23 02:29:58 +08:00
parent 2b79cf37d0
commit 66fa00aa1a
9 changed files with 539 additions and 9 deletions

View File

@ -19,6 +19,9 @@ pub struct VidaConfig {
pub star_extraction: StarExtractionConfig, pub star_extraction: StarExtractionConfig,
/// Calibration configuration /// Calibration configuration
pub calibration: CalibrationSettings, pub calibration: CalibrationSettings,
/// Polygon mask configuration for blocking ground/obstacles
#[serde(default)]
pub mask: super::mask::PolygonMaskConfig,
/// Whether to enable star extraction for sky quality check /// Whether to enable star extraction for sky quality check
pub enable_star_extraction: bool, pub enable_star_extraction: bool,
/// Minimum number of stars required for detection (skip if cloudy) /// Minimum number of stars required for detection (skip if cloudy)
@ -34,6 +37,7 @@ impl Default for VidaConfig {
line_detector: LineDetector3DConfig::default(), line_detector: LineDetector3DConfig::default(),
star_extraction: StarExtractionConfig::default(), star_extraction: StarExtractionConfig::default(),
calibration: CalibrationSettings::default(), calibration: CalibrationSettings::default(),
mask: super::mask::PolygonMaskConfig::default(),
enable_star_extraction: false, enable_star_extraction: false,
min_stars_for_detection: 500, min_stars_for_detection: 500,
} }

View File

@ -6,7 +6,8 @@
use crate::detection::vida::{ use crate::detection::vida::{
AccumulatedFrame, AccumulatorConfig, FireballDetection, FireballDetector, AccumulatedFrame, AccumulatorConfig, FireballDetection, FireballDetector,
FrameAccumulator, MeteorDetection, MeteorDetector, VidaConfig, FrameAccumulator, MeteorDetection, MeteorDetector, PolygonMask, SharedPolygonMask,
VidaConfig,
}; };
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@ -84,8 +85,21 @@ impl VidaDetectionController {
/// Create a new detection controller /// Create a new detection controller
pub fn new(config: VidaConfig, width: u32, height: u32) -> Self { pub fn new(config: VidaConfig, width: u32, height: u32) -> Self {
let accumulator = FrameAccumulator::new(width, height, config.accumulator.clone()); let accumulator = FrameAccumulator::new(width, height, config.accumulator.clone());
let fireball_detector = FireballDetector::new(config.fireball.clone()); let mut fireball_detector = FireballDetector::new(config.fireball.clone());
let meteor_detector = MeteorDetector::new(config.meteor.clone()); let mut meteor_detector = MeteorDetector::new(config.meteor.clone());
// Create and apply polygon mask if configured
if config.mask.enabled && !config.mask.polygons.is_empty() {
let mask = Arc::new(PolygonMask::from_config(config.mask.clone(), width, height));
let coverage = mask.coverage_ratio() * 100.0;
println!(
"Polygon mask enabled: {} polygons, {:.1}% coverage",
config.mask.polygons.len(),
coverage
);
fireball_detector.set_mask(Arc::clone(&mask));
meteor_detector.set_mask(mask);
}
Self { Self {
config, config,

View File

@ -8,6 +8,7 @@
use crate::detection::vida::{ use crate::detection::vida::{
AccumulatedFrame, FireballDetectorConfig, LineSegment3D, LineSegment3DDetector, Point3D, AccumulatedFrame, FireballDetectorConfig, LineSegment3D, LineSegment3DDetector, Point3D,
SharedPolygonMask,
}; };
use std::collections::HashSet; use std::collections::HashSet;
@ -48,6 +49,8 @@ impl FireballDetection {
pub struct FireballDetector { pub struct FireballDetector {
config: FireballDetectorConfig, config: FireballDetectorConfig,
line_detector: LineSegment3DDetector, line_detector: LineSegment3DDetector,
/// Optional polygon mask for filtering out ground/obstacles
polygon_mask: Option<SharedPolygonMask>,
} }
impl FireballDetector { impl FireballDetector {
@ -64,6 +67,7 @@ impl FireballDetector {
Self { Self {
config, config,
line_detector: LineSegment3DDetector::new(line_config), line_detector: LineSegment3DDetector::new(line_config),
polygon_mask: None,
} }
} }
@ -71,6 +75,16 @@ impl FireballDetector {
Self::new(FireballDetectorConfig::default()) Self::new(FireballDetectorConfig::default())
} }
/// Set the polygon mask for filtering detections
pub fn set_mask(&mut self, mask: SharedPolygonMask) {
self.polygon_mask = Some(mask);
}
/// Clear the polygon mask
pub fn clear_mask(&mut self) {
self.polygon_mask = None;
}
/// Detect fireballs in an accumulated frame /// Detect fireballs in an accumulated frame
/// ///
/// Algorithm: /// Algorithm:
@ -85,11 +99,16 @@ impl FireballDetector {
/// bottleneck is 3D line detection, not image processing. /// bottleneck is 3D line detection, not image processing.
pub fn detect(&self, frame: &AccumulatedFrame) -> Vec<FireballDetection> { pub fn detect(&self, frame: &AccumulatedFrame) -> Vec<FireballDetection> {
// Step 1: Threshold to get bright pixels at original resolution // Step 1: Threshold to get bright pixels at original resolution
let threshold_mask = frame.threshold_with_intensity( let mut threshold_mask = frame.threshold_with_intensity(
self.config.k1_threshold, self.config.k1_threshold,
self.config.min_intensity, self.config.min_intensity,
); );
// Apply polygon mask if configured
if let Some(ref mask) = self.polygon_mask {
mask.apply_to_vec(&mut threshold_mask);
}
// Step 2: Build 3D point cloud // Step 2: Build 3D point cloud
let mut point_cloud = frame.to_point_cloud(&threshold_mask); let mut point_cloud = frame.to_point_cloud(&threshold_mask);

View File

@ -0,0 +1,413 @@
//! Polygon Mask for Vida Detection
//!
//! Provides functionality to mask out regions of the image where detection
//! should be suppressed (e.g., ground, buildings, trees, artificial lights).
use serde::{Deserialize, Serialize};
use std::sync::Arc;
/// Polygon mask configuration (serializable for JSON/TOML config files)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolygonMaskConfig {
/// Whether masking is enabled
#[serde(default)]
pub enabled: bool,
/// Mask mode: exclude or include polygons
#[serde(default)]
pub mode: MaskMode,
/// List of polygons defining mask regions
#[serde(default)]
pub polygons: Vec<Polygon>,
}
impl Default for PolygonMaskConfig {
fn default() -> Self {
Self {
enabled: false,
mode: MaskMode::Exclude,
polygons: Vec::new(),
}
}
}
/// Mask mode determines how polygons affect detection
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum MaskMode {
/// Exclude mode: pixels INSIDE polygons are masked (blocked from detection)
/// Use this to mask out ground, buildings, trees, etc.
#[default]
Exclude,
/// Include mode: pixels OUTSIDE polygons are masked (only detect inside polygons)
/// Use this to limit detection to specific sky regions
Include,
}
/// A polygon defined by its vertices
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Polygon {
/// Vertex coordinates as (x, y) pixel positions
/// Vertices should form a closed polygon (last vertex connects to first)
pub vertices: Vec<(u32, u32)>,
/// Optional label for the polygon (e.g., "ground", "building", "tree")
#[serde(default)]
pub label: Option<String>,
}
impl Polygon {
/// Create a new polygon from vertices
pub fn new(vertices: Vec<(u32, u32)>) -> Self {
Self {
vertices,
label: None,
}
}
/// Create a new polygon with a label
pub fn with_label(vertices: Vec<(u32, u32)>, label: impl Into<String>) -> Self {
Self {
vertices,
label: Some(label.into()),
}
}
/// Create a rectangle polygon (convenience method)
pub fn rectangle(x: u32, y: u32, width: u32, height: u32) -> Self {
Self::new(vec![
(x, y),
(x + width, y),
(x + width, y + height),
(x, y + height),
])
}
/// Check if a point is inside the polygon using ray casting algorithm
///
/// This implements the even-odd rule: a point is inside if a ray from
/// the point crosses the polygon boundary an odd number of times.
pub fn contains(&self, x: u32, y: u32) -> bool {
if self.vertices.len() < 3 {
return false;
}
let (px, py) = (x as f32, y as f32);
let n = self.vertices.len();
let mut inside = false;
let mut j = n - 1;
for i in 0..n {
let (xi, yi) = (self.vertices[i].0 as f32, self.vertices[i].1 as f32);
let (xj, yj) = (self.vertices[j].0 as f32, self.vertices[j].1 as f32);
// Check if the ray from (px, py) going right crosses the edge (i, j)
if ((yi > py) != (yj > py))
&& (px < (xj - xi) * (py - yi) / (yj - yi) + xi)
{
inside = !inside;
}
j = i;
}
inside
}
/// Get the bounding box of the polygon as (min_x, min_y, max_x, max_y)
pub fn bounding_box(&self) -> Option<(u32, u32, u32, u32)> {
if self.vertices.is_empty() {
return None;
}
let mut min_x = u32::MAX;
let mut min_y = u32::MAX;
let mut max_x = 0u32;
let mut max_y = 0u32;
for &(x, y) in &self.vertices {
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
Some((min_x, min_y, max_x, max_y))
}
}
/// Pre-computed polygon mask bitmap for efficient runtime application
///
/// The mask is computed once at startup and stored as a bitmap where
/// `true` means the pixel is valid for detection and `false` means masked.
#[derive(Debug, Clone)]
pub struct PolygonMask {
/// Bitmap: true = valid for detection, false = masked
mask: Vec<bool>,
/// Image width
width: u32,
/// Image height
height: u32,
/// Original configuration (for visualization)
config: PolygonMaskConfig,
}
impl PolygonMask {
/// Create a polygon mask from configuration
///
/// This pre-computes the mask bitmap by testing each pixel against
/// all polygons. For a 1920x1080 image with a few simple polygons,
/// this takes approximately 50-100ms.
pub fn from_config(config: PolygonMaskConfig, width: u32, height: u32) -> Self {
let size = (width * height) as usize;
let mut mask = vec![true; size]; // Default: all pixels valid
if !config.enabled || config.polygons.is_empty() {
return Self {
mask,
width,
height,
config,
};
}
// Pre-compute bounding boxes for optimization
let bboxes: Vec<Option<(u32, u32, u32, u32)>> = config
.polygons
.iter()
.map(|p| p.bounding_box())
.collect();
for y in 0..height {
for x in 0..width {
// Check if point is in any polygon (with bounding box optimization)
let in_any_polygon = config
.polygons
.iter()
.zip(bboxes.iter())
.any(|(polygon, bbox)| {
// Quick bounding box check first
if let Some((min_x, min_y, max_x, max_y)) = bbox {
if x < *min_x || x > *max_x || y < *min_y || y > *max_y {
return false;
}
}
polygon.contains(x, y)
});
let idx = (y * width + x) as usize;
mask[idx] = match config.mode {
MaskMode::Exclude => !in_any_polygon, // Inside polygon = masked
MaskMode::Include => in_any_polygon, // Outside polygon = masked
};
}
}
Self {
mask,
width,
height,
config,
}
}
/// Create a disabled (pass-through) mask
pub fn disabled(width: u32, height: u32) -> Self {
Self::from_config(PolygonMaskConfig::default(), width, height)
}
/// Apply the mask to a binary image
///
/// Pixels that are `true` in the binary image but `false` in the mask
/// will be set to `false`.
#[inline]
pub fn apply(&self, binary: &mut super::BinaryImage) {
debug_assert_eq!(
binary.width, self.width,
"Mask width mismatch: {} vs {}",
binary.width, self.width
);
debug_assert_eq!(
binary.height, self.height,
"Mask height mismatch: {} vs {}",
binary.height, self.height
);
for (i, pixel) in binary.data.iter_mut().enumerate() {
if *pixel && !self.mask[i] {
*pixel = false;
}
}
}
/// Apply mask to a boolean threshold mask (Vec<bool>)
#[inline]
pub fn apply_to_vec(&self, threshold_mask: &mut Vec<bool>) {
debug_assert_eq!(
threshold_mask.len(),
self.mask.len(),
"Mask size mismatch"
);
for (i, pixel) in threshold_mask.iter_mut().enumerate() {
if *pixel && !self.mask[i] {
*pixel = false;
}
}
}
/// Check if a single pixel is valid for detection
#[inline]
pub fn is_valid(&self, x: u32, y: u32) -> bool {
if x >= self.width || y >= self.height {
return false;
}
let idx = (y * self.width + x) as usize;
self.mask[idx]
}
/// Get the mask bitmap directly (for advanced use)
pub fn mask_data(&self) -> &[bool] {
&self.mask
}
/// Get mask dimensions
pub fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}
/// Get the original configuration
pub fn config(&self) -> &PolygonMaskConfig {
&self.config
}
/// Count masked pixels
pub fn masked_count(&self) -> usize {
self.mask.iter().filter(|&&v| !v).count()
}
/// Count valid pixels
pub fn valid_count(&self) -> usize {
self.mask.iter().filter(|&&v| v).count()
}
/// Get mask coverage ratio (masked pixels / total pixels)
pub fn coverage_ratio(&self) -> f32 {
self.masked_count() as f32 / self.mask.len() as f32
}
}
/// Thread-safe shared polygon mask
pub type SharedPolygonMask = Arc<PolygonMask>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_polygon_contains_square() {
let square = Polygon::rectangle(100, 100, 200, 200);
// Inside
assert!(square.contains(150, 150));
assert!(square.contains(200, 200));
// Outside
assert!(!square.contains(50, 50));
assert!(!square.contains(350, 350));
// Edge cases (on boundary - implementation dependent)
assert!(square.contains(100, 150)); // Left edge
assert!(square.contains(299, 150)); // Right edge
}
#[test]
fn test_polygon_contains_triangle() {
let triangle = Polygon::new(vec![(100, 100), (200, 100), (150, 200)]);
// Inside
assert!(triangle.contains(150, 150));
// Outside
assert!(!triangle.contains(50, 50));
assert!(!triangle.contains(180, 180));
}
#[test]
fn test_mask_exclude_mode() {
let config = PolygonMaskConfig {
enabled: true,
mode: MaskMode::Exclude,
polygons: vec![Polygon::rectangle(0, 50, 100, 50)], // Mask bottom half
};
let mask = PolygonMask::from_config(config, 100, 100);
// Top half should be valid
assert!(mask.is_valid(50, 25));
// Bottom half should be masked
assert!(!mask.is_valid(50, 75));
}
#[test]
fn test_mask_include_mode() {
let config = PolygonMaskConfig {
enabled: true,
mode: MaskMode::Include,
polygons: vec![Polygon::rectangle(25, 25, 50, 50)], // Only center
};
let mask = PolygonMask::from_config(config, 100, 100);
// Center should be valid
assert!(mask.is_valid(50, 50));
// Outside should be masked
assert!(!mask.is_valid(10, 10));
assert!(!mask.is_valid(90, 90));
}
#[test]
fn test_mask_disabled() {
let config = PolygonMaskConfig {
enabled: false,
mode: MaskMode::Exclude,
polygons: vec![Polygon::rectangle(0, 0, 100, 100)],
};
let mask = PolygonMask::from_config(config, 100, 100);
// Everything should be valid when disabled
assert!(mask.is_valid(50, 50));
assert_eq!(mask.valid_count(), 10000);
}
#[test]
fn test_polygon_bounding_box() {
let polygon = Polygon::new(vec![(10, 20), (100, 30), (50, 150)]);
let bbox = polygon.bounding_box().unwrap();
assert_eq!(bbox, (10, 20, 100, 150));
}
#[test]
fn test_config_serialization() {
let config = PolygonMaskConfig {
enabled: true,
mode: MaskMode::Exclude,
polygons: vec![
Polygon::with_label(vec![(0, 900), (1920, 900), (1920, 1080), (0, 1080)], "ground"),
],
};
let json = serde_json::to_string_pretty(&config).unwrap();
println!("{}", json);
let parsed: PolygonMaskConfig = serde_json::from_str(&json).unwrap();
assert!(parsed.enabled);
assert_eq!(parsed.mode, MaskMode::Exclude);
assert_eq!(parsed.polygons.len(), 1);
assert_eq!(parsed.polygons[0].label, Some("ground".to_string()));
}
}

View File

@ -11,7 +11,8 @@
use crate::detection::vida::{ use crate::detection::vida::{
AccumulatedFrame, BinaryImage, HoughLineDetector, Line2D, LineSegment3D, AccumulatedFrame, BinaryImage, HoughLineDetector, Line2D, LineSegment3D,
LineSegment3DDetector, MeteorDetectorConfig, Morphology, Point3D, TemporalWindow, LineSegment3DDetector, MeteorDetectorConfig, Morphology, Point3D, PolygonMask,
SharedPolygonMask, TemporalWindow,
}; };
/// Meteor detection result /// Meteor detection result
@ -98,6 +99,8 @@ pub struct MeteorDetector {
morphology: Morphology, morphology: Morphology,
hough_detector: HoughLineDetector, hough_detector: HoughLineDetector,
line_3d_detector: LineSegment3DDetector, line_3d_detector: LineSegment3DDetector,
/// Optional polygon mask for filtering out ground/obstacles
polygon_mask: Option<SharedPolygonMask>,
} }
impl MeteorDetector { impl MeteorDetector {
@ -111,6 +114,7 @@ impl MeteorDetector {
morphology, morphology,
hough_detector, hough_detector,
line_3d_detector, line_3d_detector,
polygon_mask: None,
} }
} }
@ -118,6 +122,16 @@ impl MeteorDetector {
Self::new(MeteorDetectorConfig::default()) Self::new(MeteorDetectorConfig::default())
} }
/// Set the polygon mask for filtering detections
pub fn set_mask(&mut self, mask: SharedPolygonMask) {
self.polygon_mask = Some(mask);
}
/// Clear the polygon mask
pub fn clear_mask(&mut self) {
self.polygon_mask = None;
}
/// Detect meteors in an accumulated frame /// Detect meteors in an accumulated frame
pub fn detect(&self, frame: &AccumulatedFrame) -> Vec<MeteorDetection> { pub fn detect(&self, frame: &AccumulatedFrame) -> Vec<MeteorDetection> {
// Step 1: Pre-check (white pixel ratio) at original resolution // Step 1: Pre-check (white pixel ratio) at original resolution
@ -149,7 +163,12 @@ impl MeteorDetector {
// Create binary image at original resolution // Create binary image at original resolution
let window_mask = self.threshold_window_fast(&window_maxpixel, &thresholds); let window_mask = self.threshold_window_fast(&window_maxpixel, &thresholds);
let window_binary = BinaryImage::from_threshold(window_mask, frame.width, frame.height); let mut window_binary = BinaryImage::from_threshold(window_mask, frame.width, frame.height);
// Apply polygon mask if configured (before morphology to avoid edge artifacts)
if let Some(ref mask) = self.polygon_mask {
mask.apply(&mut window_binary);
}
// Key optimization: downsample binary image (not FTP data) // Key optimization: downsample binary image (not FTP data)
// OR operation preserves meteor trajectory connectivity // OR operation preserves meteor trajectory connectivity

View File

@ -22,6 +22,7 @@ pub mod star_extractor;
pub mod ftpdetect; pub mod ftpdetect;
pub mod calibration; pub mod calibration;
pub mod controller; pub mod controller;
pub mod mask;
pub use accumulated_frame::*; pub use accumulated_frame::*;
pub use config::*; pub use config::*;
@ -34,3 +35,4 @@ pub use star_extractor::*;
pub use ftpdetect::*; pub use ftpdetect::*;
pub use calibration::*; pub use calibration::*;
pub use controller::*; pub use controller::*;
pub use mask::*;

View File

@ -115,6 +115,10 @@ enum Commands {
/// Maximum frames to process (default: all frames) /// Maximum frames to process (default: all frames)
#[arg(long)] #[arg(long)]
max_frames: Option<u32>, max_frames: Option<u32>,
/// Path to polygon mask configuration file (JSON)
#[arg(long)]
mask: Option<String>,
}, },
} }
@ -203,8 +207,9 @@ async fn main() -> Result<()> {
video, video,
k1, k1,
max_frames, max_frames,
mask,
} => { } => {
if let Err(e) = test_vida(video.clone(), *k1, *max_frames).await { if let Err(e) = test_vida(video.clone(), *k1, *max_frames, mask.clone()).await {
eprintln!("❌ Vida test failed: {}", e); eprintln!("❌ Vida test failed: {}", e);
std::process::exit(1); std::process::exit(1);
} }
@ -886,8 +891,8 @@ fn draw_line(img: &mut image::RgbImage, x0: i32, y0: i32, x1: i32, y1: i32, colo
/// Test Vida four-frame meteor detection algorithm with a video file /// Test Vida four-frame meteor detection algorithm with a video file
/// Uses FFmpeg pipe for zero-disk-IO streaming /// Uses FFmpeg pipe for zero-disk-IO streaming
async fn test_vida(video_path: String, k1: f32, max_frames: Option<u32>) -> Result<()> { async fn test_vida(video_path: String, k1: f32, max_frames: Option<u32>, mask_path: Option<String>) -> Result<()> {
use crate::detection::vida::{VidaConfig, VidaDetectionController, MeteorDetectorConfig}; use crate::detection::vida::{VidaConfig, VidaDetectionController, MeteorDetectorConfig, PolygonMaskConfig};
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::io::{BufReader, Read}; use std::io::{BufReader, Read};
use std::time::Instant; use std::time::Instant;
@ -897,6 +902,9 @@ async fn test_vida(video_path: String, k1: f32, max_frames: Option<u32>) -> Resu
if let Some(max) = max_frames { if let Some(max) = max_frames {
println!("Max frames: {}", max); println!("Max frames: {}", max);
} }
if let Some(ref mask) = mask_path {
println!("Mask config: {}", mask);
}
println!(); println!();
// Normalize and validate video path // Normalize and validate video path
@ -1073,6 +1081,25 @@ async fn test_vida(video_path: String, k1: f32, max_frames: Option<u32>) -> Resu
..config.meteor ..config.meteor
}; };
// Load and apply polygon mask if specified
if let Some(ref mask_file) = mask_path {
let mask_file_path = normalize_path(mask_file);
let mask_path_buf = PathBuf::from(&mask_file_path);
if !mask_path_buf.exists() {
anyhow::bail!("Mask config file not found: {}", mask_file_path);
}
let mask_json = std::fs::read_to_string(&mask_path_buf)?;
let mask_config: PolygonMaskConfig = serde_json::from_str(&mask_json)
.map_err(|e| anyhow::anyhow!("Failed to parse mask config: {}", e))?;
println!("Loaded mask config: {} polygons, mode: {:?}, enabled: {}",
mask_config.polygons.len(),
mask_config.mode,
mask_config.enabled
);
config.mask = mask_config;
}
let mut controller = VidaDetectionController::new(config, width, height); let mut controller = VidaDetectionController::new(config, width, height);
// Processing stats // Processing stats

View File

@ -0,0 +1,10 @@
{
"enabled": true,
"mode": "exclude",
"polygons": [
{
"label": "ground",
"vertices": [[0, 900], [1920, 900], [1920, 1080], [0, 1080]]
}
]
}

View File

@ -0,0 +1,22 @@
{
"enabled": true,
"mode": "exclude",
"polygons": [
{
"label": "ground_horizon",
"vertices": [[0, 700], [1920, 700], [1920, 1080], [0, 1080]]
},
{
"label": "text_overlay",
"vertices": [[0, 590], [280, 590], [280, 670], [0, 670]]
},
{
"label": "tree_right",
"vertices": [[1750, 0], [1920, 0], [1920, 700], [1850, 700], [1800, 500], [1750, 300]]
},
{
"label": "vegetation_left",
"vertices": [[0, 550], [150, 600], [200, 700], [0, 700]]
}
]
}