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:
parent
2b79cf37d0
commit
66fa00aa1a
@ -19,6 +19,9 @@ pub struct VidaConfig {
|
||||
pub star_extraction: StarExtractionConfig,
|
||||
/// Calibration configuration
|
||||
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
|
||||
pub enable_star_extraction: bool,
|
||||
/// Minimum number of stars required for detection (skip if cloudy)
|
||||
@ -34,6 +37,7 @@ impl Default for VidaConfig {
|
||||
line_detector: LineDetector3DConfig::default(),
|
||||
star_extraction: StarExtractionConfig::default(),
|
||||
calibration: CalibrationSettings::default(),
|
||||
mask: super::mask::PolygonMaskConfig::default(),
|
||||
enable_star_extraction: false,
|
||||
min_stars_for_detection: 500,
|
||||
}
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
|
||||
use crate::detection::vida::{
|
||||
AccumulatedFrame, AccumulatorConfig, FireballDetection, FireballDetector,
|
||||
FrameAccumulator, MeteorDetection, MeteorDetector, VidaConfig,
|
||||
FrameAccumulator, MeteorDetection, MeteorDetector, PolygonMask, SharedPolygonMask,
|
||||
VidaConfig,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
@ -84,8 +85,21 @@ impl VidaDetectionController {
|
||||
/// Create a new detection controller
|
||||
pub fn new(config: VidaConfig, width: u32, height: u32) -> Self {
|
||||
let accumulator = FrameAccumulator::new(width, height, config.accumulator.clone());
|
||||
let fireball_detector = FireballDetector::new(config.fireball.clone());
|
||||
let meteor_detector = MeteorDetector::new(config.meteor.clone());
|
||||
let mut fireball_detector = FireballDetector::new(config.fireball.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 {
|
||||
config,
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
use crate::detection::vida::{
|
||||
AccumulatedFrame, FireballDetectorConfig, LineSegment3D, LineSegment3DDetector, Point3D,
|
||||
SharedPolygonMask,
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
|
||||
@ -48,6 +49,8 @@ impl FireballDetection {
|
||||
pub struct FireballDetector {
|
||||
config: FireballDetectorConfig,
|
||||
line_detector: LineSegment3DDetector,
|
||||
/// Optional polygon mask for filtering out ground/obstacles
|
||||
polygon_mask: Option<SharedPolygonMask>,
|
||||
}
|
||||
|
||||
impl FireballDetector {
|
||||
@ -64,6 +67,7 @@ impl FireballDetector {
|
||||
Self {
|
||||
config,
|
||||
line_detector: LineSegment3DDetector::new(line_config),
|
||||
polygon_mask: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,6 +75,16 @@ impl FireballDetector {
|
||||
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
|
||||
///
|
||||
/// Algorithm:
|
||||
@ -85,11 +99,16 @@ impl FireballDetector {
|
||||
/// bottleneck is 3D line detection, not image processing.
|
||||
pub fn detect(&self, frame: &AccumulatedFrame) -> Vec<FireballDetection> {
|
||||
// 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.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
|
||||
let mut point_cloud = frame.to_point_cloud(&threshold_mask);
|
||||
|
||||
|
||||
413
meteor-edge-client/src/detection/vida/mask.rs
Normal file
413
meteor-edge-client/src/detection/vida/mask.rs
Normal 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()));
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,8 @@
|
||||
|
||||
use crate::detection::vida::{
|
||||
AccumulatedFrame, BinaryImage, HoughLineDetector, Line2D, LineSegment3D,
|
||||
LineSegment3DDetector, MeteorDetectorConfig, Morphology, Point3D, TemporalWindow,
|
||||
LineSegment3DDetector, MeteorDetectorConfig, Morphology, Point3D, PolygonMask,
|
||||
SharedPolygonMask, TemporalWindow,
|
||||
};
|
||||
|
||||
/// Meteor detection result
|
||||
@ -98,6 +99,8 @@ pub struct MeteorDetector {
|
||||
morphology: Morphology,
|
||||
hough_detector: HoughLineDetector,
|
||||
line_3d_detector: LineSegment3DDetector,
|
||||
/// Optional polygon mask for filtering out ground/obstacles
|
||||
polygon_mask: Option<SharedPolygonMask>,
|
||||
}
|
||||
|
||||
impl MeteorDetector {
|
||||
@ -111,6 +114,7 @@ impl MeteorDetector {
|
||||
morphology,
|
||||
hough_detector,
|
||||
line_3d_detector,
|
||||
polygon_mask: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,6 +122,16 @@ impl MeteorDetector {
|
||||
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
|
||||
pub fn detect(&self, frame: &AccumulatedFrame) -> Vec<MeteorDetection> {
|
||||
// Step 1: Pre-check (white pixel ratio) at original resolution
|
||||
@ -149,7 +163,12 @@ impl MeteorDetector {
|
||||
|
||||
// Create binary image at original resolution
|
||||
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)
|
||||
// OR operation preserves meteor trajectory connectivity
|
||||
|
||||
@ -22,6 +22,7 @@ pub mod star_extractor;
|
||||
pub mod ftpdetect;
|
||||
pub mod calibration;
|
||||
pub mod controller;
|
||||
pub mod mask;
|
||||
|
||||
pub use accumulated_frame::*;
|
||||
pub use config::*;
|
||||
@ -34,3 +35,4 @@ pub use star_extractor::*;
|
||||
pub use ftpdetect::*;
|
||||
pub use calibration::*;
|
||||
pub use controller::*;
|
||||
pub use mask::*;
|
||||
|
||||
@ -115,6 +115,10 @@ enum Commands {
|
||||
/// Maximum frames to process (default: all frames)
|
||||
#[arg(long)]
|
||||
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,
|
||||
k1,
|
||||
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);
|
||||
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
|
||||
/// Uses FFmpeg pipe for zero-disk-IO streaming
|
||||
async fn test_vida(video_path: String, k1: f32, max_frames: Option<u32>) -> Result<()> {
|
||||
use crate::detection::vida::{VidaConfig, VidaDetectionController, MeteorDetectorConfig};
|
||||
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, PolygonMaskConfig};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::io::{BufReader, Read};
|
||||
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 {
|
||||
println!("Max frames: {}", max);
|
||||
}
|
||||
if let Some(ref mask) = mask_path {
|
||||
println!("Mask config: {}", mask);
|
||||
}
|
||||
println!();
|
||||
|
||||
// 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
|
||||
};
|
||||
|
||||
// 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);
|
||||
|
||||
// Processing stats
|
||||
|
||||
10
meteor-edge-client/test_mask.json
Normal file
10
meteor-edge-client/test_mask.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"enabled": true,
|
||||
"mode": "exclude",
|
||||
"polygons": [
|
||||
{
|
||||
"label": "ground",
|
||||
"vertices": [[0, 900], [1920, 900], [1920, 1080], [0, 1080]]
|
||||
}
|
||||
]
|
||||
}
|
||||
22
meteor-edge-client/video3_mask.json
Normal file
22
meteor-edge-client/video3_mask.json
Normal 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]]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user