feat: 把solve-field抽象出来,因为linux和mac下参数行为不一致;

This commit is contained in:
grabbit 2025-06-29 11:57:10 +08:00
parent aea16be92e
commit 1254e5e829
3 changed files with 5 additions and 241 deletions

View File

@ -224,9 +224,9 @@ impl AstrometrySolver {
}
// // Clean up temporary files if requested
// if !self.config.keep_temp_files {
// self.cleanup_temp_files(base_name).await;
// }
if !self.config.keep_temp_files {
self.cleanup_temp_files(base_name).await;
}
Ok(result)
}

View File

@ -150,7 +150,7 @@ impl Default for SolverConfig {
search_radius: None,
timeout_seconds: 60,
overwrite: true,
keep_temp_files: false,
keep_temp_files: true,
extra_args: Vec::new(),
}
}

View File

@ -299,7 +299,7 @@ impl StarChart {
.scale_range(options.index_scale_range.0, options.index_scale_range.1)
.timeout(options.max_solve_time as u32)
.overwrite(true)
.keep_temp_files(false);
.keep_temp_files(true);
// Add index files if specified
if let Some(ref index_files) = options.index_file {
@ -1473,164 +1473,6 @@ impl StarChart {
Ok((center_ra, center_dec, radius, scale))
}
/// Legacy run_astrometry method - kept for compatibility but marked as deprecated
#[deprecated(note = "Use run_astrometry_with_solver instead")]
async fn run_astrometry(
_frame: &Frame,
working_dir: &Path,
options: &StarChartOptions,
previous_hint: &Option<SolutionHint>,
_gps_status: &Arc<Mutex<GpsStatus>>
) -> Result<(SolutionHint, PathBuf, PathBuf)> {
// 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("--dir"); // Output directory
command.arg(working_dir);
command.arg("--cpulimit");
command.arg("60");
// Scale options and hints
command.arg("--scale-units").arg("arcsecperpix");
// Use previous hint if available
if let Some(hint) = previous_hint {
debug!("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 {
debug!("No previous hint available, using wider search parameters");
// For the first solve, use a much wider scale range to increase chances of success
command.arg("--scale-low").arg("70.0");
command.arg("--scale-high").arg("90.0"); // Very wide range for first solve
// Don't provide RA/Dec hints for the first solve to allow full-sky search
}
// Index path
if !options.index_path.is_empty() {
command.arg("--config").arg(Self::create_config_file(working_dir, &options.index_path)?);
}
// Use index files if specified
if let Some(index_files) = &options.index_file {
for index_file in index_files {
if std::path::Path::new(index_file).exists() {
info!("Using index file: {}", index_file);
command.arg("--index-file").arg(index_file);
} else {
warn!("Index file not found: {}", index_file);
}
}
}
// Additional options for better handling of large images
command.arg("--depth").arg("10,20,30,40,50"); // Try different numbers of stars
command.arg("--downsample").arg("2"); // Downsample large images by factor of 2
command.arg("--no-remove-lines"); // Don't remove line patterns (might be meteor trails)
command.arg("--crpix-center");
command.arg("--no-plots"); // Don't create plot files to save time
// command.arg("--verbose"); // Enable verbose output for debugging
// Execute command with timeout
// debug!("Running astrometry.net solve-field command: {:?}", command);
// Print the full command line for debugging
let full_command = format!("{} {}",
&options.solve_field_path,
command.get_args()
.map(|arg| format!("\"{}\"", arg.to_string_lossy()))
.collect::<Vec<_>>()
.join(" ")
);
// debug!("Full solve-field command: {}", full_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) => {
debug!("solve-field command finished with status: {}", output.status);
let stdout_str = String::from_utf8_lossy(&output.stdout);
let stderr_str = String::from_utf8_lossy(&output.stderr);
debug!("solve-field stdout: {}", stdout_str);
debug!("solve-field stderr: {}", stderr_str);
// The real test for success is if the WCS file was created.
let wcs_file = input_file.with_extension("wcs");
let axy_file = input_file.with_extension("axy");
if wcs_file.exists() {
// info!("WCS file found, solve succeeded.");
let solution = Self::parse_wcs_solution_new(&wcs_file)?;
let hint = SolutionHint {
ra: solution.0,
dec: solution.1,
radius: solution.2,
scale: solution.3,
timestamp: Utc::now(),
};
return Ok((hint, wcs_file, axy_file));
} else {
error!("solve-field did not produce a WCS file.");
error!("Expected WCS file at: {:?}", wcs_file);
error!("Command exit code: {}", output.status);
error!("Working directory contents:");
if let Ok(entries) = std::fs::read_dir(working_dir) {
for entry in entries {
if let Ok(entry) = entry {
error!(" - {:?}", entry.path());
}
}
}
return Err(anyhow::anyhow!(
"WCS file not found at {:?}. Command exit code: {}. stdout: {}, stderr: {}",
wcs_file,
output.status,
stdout_str,
stderr_str
));
}
}
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
error!("solve-field command timed out after {} seconds. Consider checking for and manually terminating any lingering 'solve-field' processes.", options.max_solve_time);
// TODO: Implement proper process killing on timeout
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");
@ -1645,84 +1487,6 @@ impl StarChart {
Ok(config_path)
}
/// Parse a WCS solution file to extract key parameters
fn parse_wcs_solution(wcs_file: &Path) -> Result<(f64, f64, f64, f64)> {
info!("🔍 PARSING WCS SOLUTION from file: {:?}", wcs_file);
let mut fits = FitsFile::open(wcs_file)
.context(format!("Failed to open WCS FITS file at {:?}", wcs_file))?;
let hdu = fits.primary_hdu()?;
let ra = hdu.read_key::<f64>(&mut fits, "CRVAL1").unwrap_or(0.0);
let dec = hdu.read_key::<f64>(&mut fits, "CRVAL2").unwrap_or(0.0);
info!("🌍 Field center coordinates: RA={:.6}°, Dec={:.6}°", ra, dec);
// Try to calculate scale from CD matrix first, as it's more accurate
let cd1_1 = hdu.read_key::<f64>(&mut fits, "CD1_1").ok();
let cd1_2 = hdu.read_key::<f64>(&mut fits, "CD1_2").ok();
let cd2_1 = hdu.read_key::<f64>(&mut fits, "CD2_1").ok();
let cd2_2 = hdu.read_key::<f64>(&mut fits, "CD2_2").ok();
let mut scale = 0.0;
if let (Some(cd1_1), Some(cd1_2), Some(cd2_1), Some(cd2_2)) = (cd1_1, cd1_2, cd2_1, cd2_2) {
// Pixel scale in degrees is sqrt of the determinant of the CD matrix
let determinant = (cd1_1 * cd2_2 - cd1_2 * cd2_1).abs();
scale = determinant.sqrt() * 3600.0; // Convert degrees to arcseconds
info!("📐 CD matrix scale calculation: det={:.8e}, scale={:.3} arcsec/px", determinant, scale);
info!("📐 CD matrix: [{:.8}, {:.8}] [{:.8}, {:.8}]", cd1_1, cd1_2, cd2_1, cd2_2);
} else if let Ok(cdelt1) = hdu.read_key::<f64>(&mut fits, "CDELT1") {
// Fallback to CDELT if CD matrix is not present
scale = cdelt1.abs() * 3600.0;
info!("📐 CDELT scale calculation: CDELT1={:.8}, scale={:.3} arcsec/px", cdelt1, scale);
} else {
warn!("⚠️ No CD matrix or CDELT found in WCS file");
}
// Calculate radius from image dimensions and pixel scale.
// Try IMAGEW/IMAGEH first (astrometry.net format), then fallback to NAXIS1/NAXIS2
let image_width = hdu.read_key::<i64>(&mut fits, "IMAGEW")
.or_else(|_| hdu.read_key::<i64>(&mut fits, "NAXIS1"))
.unwrap_or(0);
let image_height = hdu.read_key::<i64>(&mut fits, "IMAGEH")
.or_else(|_| hdu.read_key::<i64>(&mut fits, "NAXIS2"))
.unwrap_or(0);
info!("📏 Image dimensions: width={}, height={}", image_width, image_height);
let radius = if scale > 0.0 && image_width > 0 && image_height > 0 {
// Approximate field of view radius (diagonal / 2 * pixel_scale)
let diagonal_pixels = ((image_width as f64).powi(2) + (image_height as f64).powi(2)).sqrt();
let diagonal_arcsec = diagonal_pixels * scale;
let radius_deg = diagonal_arcsec / 2.0 / 3600.0;
info!("📐 Field radius calculation: diagonal={:.1}px, diagonal_arcsec={:.1}, radius={:.3}°",
diagonal_pixels, diagonal_arcsec, radius_deg);
radius_deg
} else {
// If we can't determine image size, use a conservative default
warn!("⚠️ Cannot calculate field radius - using default 10.0°");
if image_width == 0 && image_height == 0 {
warn!("⚠️ Both NAXIS1/NAXIS2 and IMAGEW/IMAGEH are 0!");
}
10.0 // Default radius in degrees (more reasonable for typical camera FOV)
};
info!("🎯 PARSED WCS SOLUTION: RA={:.6}°, Dec={:.6}°, Scale={:.3} arcsec/px, Radius={:.3}°",
ra, dec, scale, radius);
if ra == 0.0 && dec == 0.0 {
warn!("⚠️ CRVAL1 and CRVAL2 are both zero - this may indicate parsing failure");
return Err(anyhow::anyhow!("Failed to parse RA/Dec from WCS file - both coordinates are zero"));
}
if scale == 0.0 {
warn!("⚠️ Could not determine pixel scale from WCS file");
}
info!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Ok((ra, dec, radius, scale))
}
/// 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 {