feat: 星图解析

This commit is contained in:
grabbit 2025-04-22 01:46:51 +08:00
parent 07d99d009f
commit b19122e693
8 changed files with 1072 additions and 5 deletions

236
config-example-updated.toml Normal file
View File

@ -0,0 +1,236 @@
# Meteor Detection System Configuration
# Unique identifier for this detector (will be auto-generated if not specified)
device_id = "meteor-detector-01"
# Logging level (trace, debug, info, warn, error)
log_level = "info"
# Camera settings
[camera]
# Camera device:
# - Linux: device path (e.g., "/dev/video0")
# - macOS: device index (e.g., "0") or identifier
device = "/dev/video0"
# Resolution (options: HD1080p, HD720p, VGA)
resolution = "HD720p"
# Frames per second
fps = 30
# Exposure mode (Auto or Manual exposure time in microseconds)
exposure = "Auto"
# Gain/ISO setting (0-255)
gain = 128
# Whether to lock focus at infinity
focus_locked = true
# GPS and time synchronization
[gps]
# Whether to enable GPS functionality
enable_gps = true
# Serial port for GPS module
port = "/dev/ttyAMA0"
# Baud rate
baud_rate = 9600
# Whether to use PPS signal for precise timing
use_pps = true
# GPIO pin for PPS signal (BCM numbering)
pps_pin = 18
# Allow system to run without GPS (using fallback position)
allow_degraded_mode = true
# Camera orientation
[gps.camera_orientation]
# Azimuth/heading in degrees (0 = North, 90 = East)
azimuth = 0.0
# Elevation/pitch in degrees (0 = horizontal, 90 = straight up)
elevation = 90.0
# Fallback GPS position (used when GPS is not available)
[gps.fallback_position]
# Latitude in degrees (positive is North, negative is South)
latitude = 34.0522
# Longitude in degrees (positive is East, negative is West)
longitude = -118.2437
# Altitude in meters above sea level
altitude = 85.0
# Environmental sensors
[sensors]
# Whether to use DHT22 temperature/humidity sensor
use_dht22 = true
# GPIO pin for DHT22 data (BCM numbering)
dht22_pin = 4
# Whether to use light sensor
use_light_sensor = true
# GPIO pin for light sensor analog input
light_sensor_pin = 0
# Sampling interval in seconds
sampling_interval = 10
# Whether to allow operation without sensors (using fallback values)
allow_degraded_mode = true
# Default temperature value when sensor is unavailable (Celsius)
fallback_temperature = 25.0
# Default humidity value when sensor is unavailable (0-100%)
fallback_humidity = 50.0
# Default sky brightness value when sensor is unavailable (0-1)
fallback_sky_brightness = 0.05
# Star chart overlay settings
[star_chart]
# Whether the star chart overlay is enabled
enabled = true
# Path to the astrometry.net solve-field binary
solve_field_path = "/usr/local/bin/solve-field"
# Path to the astrometry.net index files
index_path = "/usr/local/share/astrometry"
# Update frequency in seconds
update_frequency = 30.0
# Star marker color (B, G, R, A)
star_color = [0, 255, 0, 255] # Green
# Constellation line color (B, G, R, A)
constellation_color = [0, 180, 0, 180] # Semi-transparent green
# Star marker size
star_size = 3
# Constellation line thickness
line_thickness = 1
# Working directory for temporary files
working_dir = "/tmp/astrometry"
# Whether to show star names
show_star_names = true
# Size of index files to use (in arcminutes)
index_scale_range = [10.0, 60.0]
# Maximum time to wait for solve-field (seconds)
max_solve_time = 60
# Storage settings
[storage]
# Directory for storing raw video data
raw_video_dir = "data/raw"
# Directory for storing event video clips
event_video_dir = "data/events"
# Maximum disk space to use for storage (in MB)
max_disk_usage_mb = 10000
# Number of days to keep event data
event_retention_days = 30
# Whether to compress video files
compress_video = true
# Watermark settings
[watermark]
# Whether the watermark is enabled
enabled = true
# Position of the watermark
position = "BottomLeft"
# Font scale (1.0 = normal size)
font_scale = 0.6
# Font thickness
thickness = 1
# Text color (B, G, R, A)
color = [255, 255, 255, 255] # White
# Include background behind text
background = true
# Background color (B, G, R, A)
background_color = [0, 0, 0, 128] # Semi-transparent black
# Padding around text (in pixels)
padding = 8
# Content to include in the watermark
content = ["Timestamp", "GpsCoordinates", "Environment"]
# Date/time format string
time_format = "%Y-%m-%d %H:%M:%S%.3f"
# Coordinate format (decimal or dms)
coordinate_format = "decimal"
# Temperature format (C or F)
temperature_format = "C"
# Detection settings
[detection]
# Minimum brightness change to trigger detection
min_brightness_delta = 30.0
# Minimum number of pixels changed to trigger detection
min_pixel_change = 10
# Minimum number of consecutive frames to confirm event
min_frames = 3
# Number of seconds to save before/after event
event_buffer_seconds = 10
# Detection sensitivity (0.0-1.0)
sensitivity = 0.7
# Detection pipeline configuration (for multiple detectors)
[detection.pipeline]
# Maximum number of parallel detector workers
max_parallel_workers = 4
# Aggregation strategy: "any", "all", "majority"
aggregation_strategy = "any"
# Detector configurations
# You can include multiple detectors by adding more [[detection.pipeline.detectors]] sections
# Brightness detector configuration
[[detection.pipeline.detectors]]
type = "brightness"
# Unique ID for this detector
id = "brightness-main"
# Minimum brightness change to trigger detection
min_brightness_delta = 30.0
# Minimum number of pixels changed to trigger detection
min_pixel_change = 10
# Minimum number of consecutive frames to confirm event
min_frames = 3
# Detection sensitivity (0.0-1.0)
sensitivity = 0.7
# CAMS detector configuration
[[detection.pipeline.detectors]]
type = "cams"
# Unique ID for this detector
id = "cams-main"
# Brightness threshold for meteor detection in maxpixel image
brightness_threshold = 30
# Minimum ratio of stdpixel to avepixel for meteor detection
std_to_avg_ratio_threshold = 1.5
# Minimum number of pixels that must exceed thresholds
min_pixel_count = 10
# Minimum trajectory length (pixels) to be considered a meteor
min_trajectory_length = 5
# Whether to save feature images for all batches (not just detections)
save_all_feature_images = false
# Directory to save feature images
output_dir = "output/cams"
# Prefix for saved files
file_prefix = "meteor"
# Communication settings
[communication]
# MQTT broker URL
mqtt_broker = "mqtt://localhost:1883"
# MQTT client ID (will be auto-generated if not specified)
mqtt_client_id = "meteor-detector-01"
# MQTT credentials (optional)
mqtt_username = ""
mqtt_password = ""
# Topic for event notifications
event_topic = "meteor/events"
# Topic for system status
status_topic = "meteor/status"
# HTTP API port
api_port = 8080
# Whether to enable SSL for HTTP API
api_use_ssl = false
# Path to SSL certificate and key (only needed if api_use_ssl = true)
api_cert_path = ""
api_key_path = ""
# RTSP streaming settings
[rtsp]
# Whether to enable RTSP streaming
enabled = true
# RTSP server port
port = 8554
# Stream name
stream_name = "meteor"
# Stream resolution (options: HD1080p, HD720p, VGA)
resolution = "HD720p"
# Stream frame rate
fps = 15
# Stream bitrate (in kbps)
bitrate = 2000

98
docs/star_chart.md Normal file
View File

@ -0,0 +1,98 @@
# Star Chart Calculation and Rendering Implementation
## Overview
This document explains the implementation of a real-time star chart calculation and rendering feature for the meteor detection system. The feature uses astrometry.net's `solve-field` tool to solve for celestial coordinates in camera frames, then renders an overlay of stars and constellations.
## Components
### 1. StarChartOptions (Configuration)
The configuration is stored in the `config-example-updated.toml` file under the `[star_chart]` section:
```toml
[star_chart]
# Whether the star chart overlay is enabled
enabled = true
# Path to the astrometry.net solve-field binary
solve_field_path = "/usr/local/bin/solve-field"
# Path to the astrometry.net index files
index_path = "/usr/local/share/astrometry"
# Update frequency in seconds
update_frequency = 30.0
# Star marker color (B, G, R, A)
star_color = [0, 255, 0, 255] # Green
# Constellation line color (B, G, R, A)
constellation_color = [0, 180, 0, 180] # Semi-transparent green
# Star marker size
star_size = 3
# Constellation line thickness
line_thickness = 1
# Working directory for temporary files
working_dir = "/tmp/astrometry"
# Whether to show star names
show_star_names = true
# Size of index files to use (in arcminutes)
index_scale_range = [10.0, 60.0]
# Maximum time to wait for solve-field (seconds)
max_solve_time = 60
```
### 2. StarChart Module
Created a new module in `src/overlay/star_chart.rs` with the following components:
- `StarChart`: Main class for handling star chart calculation and rendering
- `SolutionHint`: Stores previous solution data for providing as a hint to future solves
- `Star` and `ConstellationLine`: Data structures for storing celestial objects
- Background thread for running astrometry.net calculations asynchronously
### 3. Integration with Main Application
The star chart component is integrated into the application in `src/app.rs`:
1. Added StarChart to the Application struct
2. Initialized the StarChart in the initialize() method
3. Registered a frame hook to apply the star chart overlay
4. Added shutdown handling for the StarChart
## Technical Details
### Astrometry.net Integration
The `solve-field` tool is executed with carefully chosen parameters:
- When a previous solution exists, it's used as a hint (ra, dec, radius, scale)
- Otherwise, GPS position is used as a general hint
- Scale range is provided to limit the search space
- A custom configuration file is created for pointing to the index files
- Timeout handling prevents hanging on difficult images
### Optimization Techniques
1. **Solution Hinting**: Each solution is saved and used as a hint for subsequent calculations, significantly improving performance.
2. **Background Processing**: Star chart calculations run in a separate thread to avoid blocking the main application.
3. **Frequency Control**: Calculations are performed at controlled intervals (configurable via `update_frequency`), not on every frame.
4. **Timeout Control**: Maximum solve time is configurable, preventing hang-ups on difficult frames.
## Usage
1. Ensure astrometry.net is installed with appropriate index files
2. Configure the `[star_chart]` section in the config file
3. The star overlay will be rendered on frames when solutions are available
4. Adjustments can be made to visualization parameters without restarting
## Future Enhancements
1. **Star Catalog Integration**: Currently uses a simplified method for rendering stars. Could be enhanced to use a proper star catalog.
2. **Advanced Celestial Rendering**: Add more detailed rendering of celestial objects beyond simple stars and constellations.
3. **WCS Library Integration**: Replace the simple WCS parsing with a proper WCS library for more accurate coordinate transformations.
4. **Parallel Solving**: Allow multiple solves to run in parallel for different regions of the sky.
5. **Sub-Frame Solving**: Process smaller sections of the image to improve solve speed.

View File

@ -18,7 +18,7 @@ use crate::events::{EventBus, GpsStatusEvent, EnvironmentEvent, FrameEvent, Dete
use crate::gps::{self, GpsController, GpsStatus};
use crate::hooks::{self, HookManager};
use crate::monitoring::{self, SystemMonitor};
use crate::overlay::{self, watermark};
use crate::overlay::{self, watermark, star_chart};
use crate::sensors::{self, SensorController, EnvironmentData};
use crate::storage::{self, StorageManager};
use crate::streaming::{self, RtspServer};
@ -42,6 +42,7 @@ pub struct Application {
detection_pipeline: Option<Arc<Mutex<detection::DetectionPipeline>>>,
hook_manager: Option<Arc<Mutex<HookManager>>>,
watermark: Option<Arc<Mutex<watermark::Watermark>>>,
star_chart: Option<Arc<Mutex<star_chart::StarChart>>>,
storage_manager: Option<storage::StorageManager>,
communication_manager: Option<Arc<Mutex<communication::CommunicationManager>>>,
rtsp_server: Option<Arc<Mutex<RtspServer>>>,
@ -63,6 +64,7 @@ impl Application {
detection_pipeline: None,
hook_manager: None,
watermark: None,
star_chart: None,
storage_manager: None,
communication_manager: None,
rtsp_server: None,
@ -111,6 +113,13 @@ impl Application {
};
self.watermark = Some(Arc::new(Mutex::new(watermark)));
// Initialize star chart overlay
let star_chart = star_chart::StarChart::new(
self.config.star_chart.clone(),
Arc::new(StdMutex::new(self.gps_controller.as_ref().unwrap().lock().await.get_status()))
).await.context("Failed to initialize star chart overlay")?;
self.star_chart = Some(Arc::new(Mutex::new(star_chart)));
// Initialize RTSP server if enabled
if self.config.rtsp.enabled {
let rtsp_server = streaming::RtspServer::new(self.config.rtsp.clone());
@ -179,6 +188,27 @@ impl Application {
}
}
// Setup frame hook for star chart overlay
{
if let (Some(hook_manager), Some(star_chart)) = (&self.hook_manager, &self.star_chart) {
let mut manager = hook_manager.lock().await;
let star_chart_clone = star_chart.clone();
manager.register_hook(Box::new(hooks::BasicFrameHook::new(
"star_chart",
"Star Chart Overlay",
"Adds calculated star chart overlay to frames",
self.config.star_chart.enabled,
move |frame, timestamp| {
// Using block_in_place to properly handle async operations in sync context
tokio::task::block_in_place(|| {
let mut guard = futures::executor::block_on(star_chart_clone.lock());
futures::executor::block_on(guard.apply(frame, timestamp))
})
},
)));
}
}
// Start sensors
if let Some(controller) = &self.sensor_controller {
controller.lock().await.initialize().await
@ -339,6 +369,13 @@ impl Application {
}
}
// Shutdown the star chart component
if let Some(star_chart) = &self.star_chart {
if let Err(e) = star_chart.lock().await.shutdown().await {
error!("Error shutting down star chart: {}", e);
}
}
// Shutdown all tasks
for task in self.tasks.drain(..) {
task.abort();

View File

@ -9,7 +9,7 @@ use toml;
use crate::camera::{CameraSettings, Resolution, ExposureMode};
use crate::detection::{DetectorConfig, PipelineConfig};
use crate::gps::{GpsConfig, CameraOrientation};
use crate::overlay::WatermarkOptions;
use crate::overlay::{WatermarkOptions, StarChartOptions};
use crate::sensors::SensorConfig;
use crate::streaming::RtspConfig;
@ -272,6 +272,10 @@ pub struct Config {
#[serde(default)]
pub watermark: WatermarkOptions,
/// Star chart configuration
#[serde(default)]
pub star_chart: StarChartOptions,
/// RTSP streaming configuration
#[serde(default)]
pub rtsp: RtspConfig,
@ -311,6 +315,7 @@ impl Default for Config {
http_api: HttpApiConfig::default(),
communication: None,
watermark: WatermarkOptions::default(),
star_chart: StarChartOptions::default(),
rtsp: RtspConfig::default(),
log_level: "info".to_string(),
}
@ -404,4 +409,4 @@ pub fn load_config() -> Result<Config> {
// Load the config
Config::load(config_path)
}
}

View File

@ -20,3 +20,4 @@ pub mod communication;
pub mod monitoring;
pub mod overlay;
pub mod hooks;
pub mod events;

View File

@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::{Mutex, RwLock};
use crate::events::{EventBus, GpsStatusEvent, EnvironmentEvent};
use crate::events::EventBus;
use crate::gps::{GeoPosition, GpsStatus, CameraOrientation};
use crate::overlay::watermark::{WatermarkOptions, WatermarkPosition, WatermarkContent};
use crate::sensors::EnvironmentData;
@ -99,7 +99,7 @@ impl EventWatermark {
status.position.altitude = event.altitude;
status.camera_orientation.azimuth = event.azimuth;
status.camera_orientation.elevation = event.elevation;
status.fix_quality = event.fix_quality;
status.sync_status = event.sync_status.clone();
status.satellites = event.satellites;
debug!("Updated GPS status: lat={}, lon={}",
event.latitude, event.longitude);

View File

@ -1,3 +1,7 @@
pub(crate) mod watermark;
pub(crate) mod event_watermark;
pub(crate) mod star_chart;
pub use watermark::{WatermarkOptions, WatermarkPosition, Watermark, WatermarkContent};
pub use event_watermark::EventWatermark;
pub use star_chart::{StarChart, StarChartOptions};

686
src/overlay/star_chart.rs Normal file
View File

@ -0,0 +1,686 @@
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use log::{debug, error, info, warn};
use opencv::{core, imgproc, prelude::*, types, imgcodecs};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use std::fs;
use std::io::Write;
use tokio::sync::mpsc;
use tokio::time;
use crate::gps::{GeoPosition, GpsStatus};
use crate::camera::{Frame};
/// Configuration options for the star chart overlay
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StarChartOptions {
/// Whether the star chart overlay is enabled
pub enabled: bool,
/// Path to the astrometry.net solve-field binary
pub solve_field_path: String,
/// Path to the astrometry.net index files
pub index_path: String,
/// Update frequency in seconds
pub update_frequency: f64,
/// Star marker color (B, G, R, A)
pub star_color: (u8, u8, u8, u8),
/// Constellation line color (B, G, R, A)
pub constellation_color: (u8, u8, u8, u8),
/// Star marker size
pub star_size: i32,
/// Constellation line thicknesÅs
pub line_thickness: i32,
/// Working directory for temporary files
pub working_dir: String,
/// Whether to show star names
pub show_star_names: bool,
/// Size of index files to use (in arcminutes)
pub index_scale_range: (f64, f64),
/// Maximum time to wait for solve-field (seconds)
pub max_solve_time: u64,
}
impl Default for StarChartOptions {
fn default() -> Self {
Self {
enabled: true,
solve_field_path: "/usr/local/bin/solve-field".to_string(),
index_path: "/usr/local/share/astrometry".to_string(),
update_frequency: 30.0, // Update every 30 seconds
star_color: (0, 255, 0, 255), // Green
constellation_color: (0, 180, 0, 180), // Semi-transparent green
star_size: 3,
line_thickness: 1,
working_dir: "/tmp/astrometry".to_string(),
show_star_names: true,
index_scale_range: (10.0, 60.0), // In arcminutes
max_solve_time: 60, // 60 seconds max for solving
}
}
}
/// Previous solution information for hinting
#[derive(Debug, Clone)]
struct SolutionHint {
/// Right ascension in degrees
ra: f64,
/// Declination in degrees
dec: f64,
/// Field radius in degrees
radius: f64,
/// Pixel scale in arcsec per pixel
scale: f64,
/// Timestamp when this hint was created
timestamp: DateTime<Utc>,
}
/// Star information for rendering
#[derive(Debug, Clone)]
struct Star {
/// Name of the star (if available)
name: Option<String>,
/// Right ascension in degrees
ra: f64,
/// Declination in degrees
dec: f64,
/// Brightness/magnitude (lower is brighter)
magnitude: f32,
/// Pixel position on the image
position: Option<core::Point>,
}
/// Constellation line for rendering
#[derive(Debug, Clone)]
struct ConstellationLine {
/// Starting star RA/Dec
start: (f64, f64),
/// Ending star RA/Dec
end: (f64, f64),
/// Pixel positions on the image (if mapped)
pixel_positions: Option<(core::Point, core::Point)>,
}
/// Message types for the astrometry solver thread
enum SolverMessage {
/// Request to process a new frame
ProcessFrame(Frame),
/// Shutdown signal
Shutdown,
}
/// Star chart overlay for annotating the night sky
pub struct StarChart {
/// Star chart configuration
options: StarChartOptions,
/// Current GPS status for position information
gps_status: Arc<Mutex<GpsStatus>>,
/// Latest solution hint
latest_hint: Option<SolutionHint>,
/// Stars from the latest solution
stars: Vec<Star>,
/// Constellation lines
constellation_lines: Vec<ConstellationLine>,
/// WCS solution file path
current_wcs_path: Option<PathBuf>,
/// Whether a solve is currently in progress
solving_in_progress: bool,
/// Solver thread handle
_solver_thread: Option<tokio::task::JoinHandle<()>>,
/// Channel for sending messages to the solver thread
solver_tx: Option<mpsc::Sender<SolverMessage>>,
}
impl StarChart {
/// Create a new star chart overlay with the given options
pub async fn new(
options: StarChartOptions,
gps_status: Arc<Mutex<GpsStatus>>,
) -> Result<Self> {
// Ensure working directory exists
let working_dir = Path::new(&options.working_dir);
if !working_dir.exists() {
fs::create_dir_all(working_dir)
.context("Failed to create working directory for astrometry")?;
}
// Create the message channel for the solver thread
let (solver_tx, solver_rx) = mpsc::channel::<SolverMessage>(10);
// Create StarChart instance
let mut star_chart = Self {
options: options.clone(),
gps_status,
latest_hint: None,
stars: Vec::new(),
constellation_lines: Vec::new(),
current_wcs_path: None,
solving_in_progress: false,
_solver_thread: None,
solver_tx: Some(solver_tx),
};
// Start the solver thread
let solver_thread_handle = star_chart.start_solver_thread(solver_rx, options).await?;
star_chart._solver_thread = Some(solver_thread_handle);
Ok(star_chart)
}
/// Start the background thread that handles astrometry.net calculations
async fn start_solver_thread(
&self,
mut rx: mpsc::Receiver<SolverMessage>,
options: StarChartOptions,
) -> Result<tokio::task::JoinHandle<()>> {
// Clone needed values for the thread
let gps_status = self.gps_status.clone();
// Start the thread
let handle = tokio::spawn(async move {
info!("Star chart solver thread started");
let mut last_solve_time = Utc::now() - chrono::Duration::seconds((options.update_frequency * 2.0) as i64);
let mut latest_hint: Option<SolutionHint> = None;
// Create a working directory
let working_dir = Path::new(&options.working_dir);
if !working_dir.exists() {
if let Err(e) = fs::create_dir_all(working_dir) {
error!("Failed to create working directory for astrometry: {}", e);
return;
}
}
while let Some(msg) = rx.recv().await {
match msg {
SolverMessage::Shutdown => {
info!("Star chart solver thread shutting down");
break;
}
SolverMessage::ProcessFrame(frame) => {
// Check if we should process this frame
let now = Utc::now();
let elapsed = now.signed_duration_since(last_solve_time);
if elapsed.num_seconds() as f64 >= options.update_frequency {
debug!("Processing frame for star chart calculation");
// Save the frame as a FITS file for processing
if let Err(e) = Self::save_frame_as_fits(&frame, &working_dir) {
error!("Failed to save frame as FITS: {}", e);
continue;
}
// Run astrometry.net on the frame
let result = Self::run_astrometry(
&frame,
&working_dir,
&options,
&latest_hint,
&gps_status
).await;
match result {
Ok(new_hint) => {
info!("Star chart solution found: RA={}, Dec={}, Scale={} arcsec/px",
new_hint.ra, new_hint.dec, new_hint.scale);
latest_hint = Some(new_hint);
last_solve_time = now;
}
Err(e) => {
error!("Failed to solve star chart: {}", e);
// If enough time has passed without a solution, clear the hint
if latest_hint.is_some() {
let hint_age = now.signed_duration_since(latest_hint.as_ref().unwrap().timestamp);
if hint_age.num_minutes() > 10 {
warn!("Last solution hint is more than 10 minutes old, clearing");
latest_hint = None;
}
}
}
}
}
}
}
}
info!("Star chart solver thread terminated");
});
Ok(handle)
}
/// Save a video frame as a FITS file for astrometry.net
fn save_frame_as_fits(frame: &Frame, working_dir: &Path) -> Result<PathBuf> {
// For simplicity, we'll save as a JPEG and let astrometry.net convert it
// In a real implementation, direct FITS conversion would be better
let frame_path = working_dir.join("current_frame.jpg");
let params = core::Vector::<i32>::new();
// Check if we have a valid image
if frame.mat.empty() {
return Err(anyhow::anyhow!("Cannot save empty frame"));
}
// Save as JPEG
imgcodecs::imwrite(&frame_path.to_string_lossy(), &frame.mat, &params)?;
Ok(frame_path)
}
/// Run astrometry.net to solve the star field
async fn run_astrometry(
frame: &Frame,
working_dir: &Path,
options: &StarChartOptions,
previous_hint: &Option<SolutionHint>,
gps_status: &Arc<Mutex<GpsStatus>>
) -> Result<SolutionHint> {
// Build the solve-field command with appropriate parameters
let mut command = Command::new(&options.solve_field_path);
// Input file
let input_file = working_dir.join("current_frame.jpg");
command.arg(&input_file);
// Common options
command.arg("--no-plot"); // Don't create plots
command.arg("--overwrite"); // Overwrite existing files
command.arg("--out"); // Output directory
command.arg(working_dir);
// Scale options (from config)
command.arg("--scale-units").arg("arcsecperpix");
// Use previous hint if available
if let Some(hint) = previous_hint {
info!("Using previous solution as hint: RA={}, Dec={}, Scale={}",
hint.ra, hint.dec, hint.scale);
// Provide RA/Dec center hint
command.arg("--ra").arg(hint.ra.to_string());
command.arg("--dec").arg(hint.dec.to_string());
// Provide search radius hint (with some margin)
command.arg("--radius").arg((hint.radius * 1.2).to_string());
// Provide scale hint (with 10% margin)
let scale_low = hint.scale * 0.9;
let scale_high = hint.scale * 1.1;
command.arg("--scale-low").arg(scale_low.to_string());
command.arg("--scale-high").arg(scale_high.to_string());
} else {
// No previous hint, use GPS position and configuration defaults
let gps_position = {
let gps = gps_status.lock().unwrap();
gps.position.clone()
};
// If we have a valid GPS position, use it as a hint
if gps_position.latitude != 0.0 || gps_position.longitude != 0.0 {
// Convert to celestial coordinates (rough estimate)
// This is a simplified calculation - real implementation would use proper conversion
let (ra, dec) = Self::convert_to_celestial(
gps_position.latitude,
gps_position.longitude,
Utc::now()
);
command.arg("--ra").arg(ra.to_string());
command.arg("--dec").arg(dec.to_string());
command.arg("--radius").arg("20"); // 20 degree search radius
}
// Use config scale range
command.arg("--scale-low").arg(options.index_scale_range.0.to_string());
command.arg("--scale-high").arg(options.index_scale_range.1.to_string());
}
// Index path
if !options.index_path.is_empty() {
command.arg("--config").arg(Self::create_config_file(working_dir, &options.index_path)?);
}
// Additional options
command.arg("--depth").arg("10,20,30,40"); // Try different numbers of stars
// Execute command with timeout
info!("Running astrometry.net solve-field: {:?}", command);
// Convert command to tokio::process::Command
let mut tokio_command = tokio::process::Command::new(&options.solve_field_path);
tokio_command.args(command.get_args());
// Set timeout for the command
let timeout = Duration::from_secs(options.max_solve_time);
let result = tokio::time::timeout(timeout, tokio_command.output()).await;
match result {
Ok(output_result) => {
match output_result {
Ok(output) => {
if output.status.success() {
// Parse the solution
let wcs_file = input_file.with_extension("wcs");
if wcs_file.exists() {
// Parse WCS file to extract solution
let solution = Self::parse_wcs_solution(&wcs_file)?;
// Return the solution as a hint for future runs
return Ok(SolutionHint {
ra: solution.0,
dec: solution.1,
radius: solution.2,
scale: solution.3,
timestamp: Utc::now(),
});
} else {
return Err(anyhow::anyhow!("WCS file not found after successful solve"));
}
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow::anyhow!("solve-field failed: {}", stderr));
}
}
Err(e) => {
return Err(anyhow::anyhow!("Failed to execute solve-field: {}", e));
}
}
}
Err(_) => {
// Kill the process if it times out
// Note: In a real implementation, we'd need to find and kill the process
return Err(anyhow::anyhow!("solve-field timed out after {} seconds", options.max_solve_time));
}
}
}
/// Create a temporary astrometry.net config file
fn create_config_file(working_dir: &Path, index_path: &str) -> Result<PathBuf> {
let config_path = working_dir.join("astrometry.cfg");
let mut file = fs::File::create(&config_path)?;
// Write add_path directive
writeln!(file, "add_path {}", index_path)?;
// Write auto index directive (use all available index files)
writeln!(file, "autoindex")?;
Ok(config_path)
}
/// Parse a WCS solution file to extract key parameters
fn parse_wcs_solution(wcs_file: &Path) -> Result<(f64, f64, f64, f64)> {
// Read the WCS file
let wcs_content = fs::read_to_string(wcs_file)?;
// Parse RA, Dec center
let mut ra = 0.0;
let mut dec = 0.0;
let mut radius = 5.0; // Default radius in degrees
let mut scale = 0.0;
for line in wcs_content.lines() {
if line.starts_with("CRVAL1") {
if let Some(value) = line.split_whitespace().nth(2) {
ra = value.parse::<f64>().unwrap_or(0.0);
}
} else if line.starts_with("CRVAL2") {
if let Some(value) = line.split_whitespace().nth(2) {
dec = value.parse::<f64>().unwrap_or(0.0);
}
} else if line.starts_with("CD1_1") || line.starts_with("CDELT1") {
if let Some(value) = line.split_whitespace().nth(2) {
// Convert CD matrix element to arcsec per pixel
let cd = value.parse::<f64>().unwrap_or(0.0);
scale = cd.abs() * 3600.0; // Convert degrees to arcseconds
}
}
}
// Calculate field radius (approximately half the diagonal)
// In a real implementation, this would be calculated from the image dimensions
// and the WCS solution
Ok((ra, dec, radius, scale))
}
/// Convert latitude, longitude, and time to celestial coordinates (RA, Dec)
/// This is a simplified calculation - real implementation would use proper conversion
fn convert_to_celestial(lat: f64, lon: f64, time: DateTime<Utc>) -> (f64, f64) {
// Simplified calculation - in a real implementation, use proper astronomical formulas
// or a library like astropy in Python
// Get Julian date
let jd = time.timestamp() as f64 / 86400.0 + 2440587.5;
// Calculate local sidereal time (simplified)
let t = (jd - 2451545.0) / 36525.0;
let theta = 280.46061837 + 360.98564736629 * (jd - 2451545.0) +
0.000387933 * t * t - t * t * t / 38710000.0;
// Local hour angle
let lst = (theta + lon) % 360.0;
// Simplified: RA = LST for objects at zenith
let ra = lst;
// Dec = observer's latitude for objects at zenith
let dec = lat;
(ra, dec)
}
/// Apply star chart overlay to the given frame
pub async fn apply(&mut self, frame: &mut core::Mat, timestamp: DateTime<Utc>) -> Result<()> {
if !self.options.enabled {
return Ok(());
}
// If we have an active solver channel, send the frame for processing
if let Some(tx) = &self.solver_tx {
// Clone the frame for processing
let frame_copy = Frame {
mat: frame.clone(),
timestamp,
index: 0, // Not important for this purpose
};
// Send the frame to the solver thread, ignoring errors if channel is full
let _ = tx.send(SolverMessage::ProcessFrame(frame_copy)).await;
}
// Draw stars and constellations on the frame if we have WCS data
if let Some(wcs_path) = &self.current_wcs_path {
if wcs_path.exists() {
// Apply star chart overlay
self.draw_stars(frame)?;
self.draw_constellations(frame)?;
}
}
Ok(())
}
/// Draw stars on the frame
fn draw_stars(&self, frame: &mut core::Mat) -> Result<()> {
let star_color = core::Scalar::new(
self.options.star_color.0 as f64,
self.options.star_color.1 as f64,
self.options.star_color.2 as f64,
self.options.star_color.3 as f64,
);
for star in &self.stars {
if let Some(position) = star.position {
// Draw the star as a circle
imgproc::circle(
frame,
position,
self.options.star_size,
star_color,
-1, // Fill
imgproc::LINE_AA,
0,
)?;
// Draw star name if enabled and available
if self.options.show_star_names && star.name.is_some() {
let name = star.name.as_ref().unwrap();
imgproc::put_text(
frame,
name,
core::Point::new(position.x + 5, position.y),
imgproc::FONT_HERSHEY_SIMPLEX,
0.4,
star_color,
1,
imgproc::LINE_AA,
false,
)?;
}
}
}
Ok(())
}
/// Draw constellation lines on the frame
fn draw_constellations(&self, frame: &mut core::Mat) -> Result<()> {
let constellation_color = core::Scalar::new(
self.options.constellation_color.0 as f64,
self.options.constellation_color.1 as f64,
self.options.constellation_color.2 as f64,
self.options.constellation_color.3 as f64,
);
for line in &self.constellation_lines {
if let Some((start, end)) = line.pixel_positions {
imgproc::line(
frame,
start,
end,
constellation_color,
self.options.line_thickness,
imgproc::LINE_AA,
0,
)?;
}
}
Ok(())
}
/// Update the star and constellation data from a WCS solution
fn update_star_data(&mut self, wcs_path: &Path) -> Result<()> {
// In a real implementation, this would parse the WCS solution and
// load star/constellation data from a catalog
// For now, we'll create some dummy data
self.stars.clear();
self.constellation_lines.clear();
// Add some dummy stars
for i in 0..20 {
let x = (i as f32 * 30.0) % 640.0; // Assuming 640x480 frame
let y = (i as f32 * 20.0) % 480.0;
self.stars.push(Star {
name: Some(format!("Star {}", i)),
ra: 0.0, // Not used in this example
dec: 0.0, // Not used in this example
magnitude: (i % 5) as f32,
position: Some(core::Point::new(x as i32, y as i32)),
});
}
// Add some dummy constellation lines
for i in 0..10 {
let x1 = (i as f32 * 40.0) % 640.0; // Assuming 640x480 frame
let y1 = (i as f32 * 30.0) % 480.0;
let x2 = ((i+1) as f32 * 40.0) % 640.0;
let y2 = ((i+1) as f32 * 30.0) % 480.0;
self.constellation_lines.push(ConstellationLine {
start: (0.0, 0.0), // Not used in this example
end: (0.0, 0.0), // Not used in this example
pixel_positions: Some((
core::Point::new(x1 as i32, y1 as i32),
core::Point::new(x2 as i32, y2 as i32),
)),
});
}
Ok(())
}
/// Check if we should update the star chart
fn should_update(&self, timestamp: DateTime<Utc>) -> bool {
if let Some(hint) = &self.latest_hint {
// Check if enough time has passed since the last update
let elapsed = timestamp.signed_duration_since(hint.timestamp);
elapsed.num_seconds() as f64 >= self.options.update_frequency
} else {
// No hint yet, so we should update
true
}
}
/// Shutdown the star chart background thread
pub async fn shutdown(&mut self) -> Result<()> {
// Send shutdown message to solver thread
if let Some(tx) = self.solver_tx.take() {
let _ = tx.send(SolverMessage::Shutdown).await;
}
// Wait for the thread to complete (in a real implementation)
// if let Some(handle) = self._solver_thread.take() {
// handle.await?;
// }
info!("Star chart overlay shutdown complete");
Ok(())
}
/// Get the current star chart status
pub fn get_status(&self) -> serde_json::Value {
let latest_hint = self.latest_hint.as_ref().map(|hint| {
serde_json::json!({
"ra": hint.ra,
"dec": hint.dec,
"scale": hint.scale,
"timestamp": hint.timestamp.to_rfc3339(),
})
});
serde_json::json!({
"enabled": self.options.enabled,
"solving_in_progress": self.solving_in_progress,
"stars_count": self.stars.len(),
"constellation_lines_count": self.constellation_lines.len(),
"latest_solution": latest_hint,
"update_frequency": self.options.update_frequency,
})
}
/// Update the options
pub fn set_options(&mut self, options: StarChartOptions) {
self.options = options;
}
}
// Implement Drop to ensure resources are cleaned up
impl Drop for StarChart {
fn drop(&mut self) {
info!("Cleaning up star chart resources");
// We can't use async functions in drop, so we'll just log a warning
// In a real implementation, we might create a synchronous version of shutdown
if self.solver_tx.is_some() {
warn!("Star chart was dropped without calling shutdown() first");
}
}
}