pub struct ImageAnalyzer { /* private fields */ }Expand description
Image analyzer with builder pattern.
Implementations§
Source§impl ImageAnalyzer
impl ImageAnalyzer
Sourcepub fn new() -> Self
pub fn new() -> Self
Examples found in repository?
3fn main() {
4 let dir = "tests/no_trails";
5 let mut files: Vec<_> = std::fs::read_dir(dir)
6 .unwrap()
7 .filter_map(|e| e.ok())
8 .filter(|e| e.path().extension().map_or(false, |ext| ext == "fits"))
9 .map(|e| e.path())
10 .collect();
11 files.sort();
12
13 println!(
14 "{:<12} {:>5} {:>5} {:>6} {:>6} {:>6} {:>6} {:>7} {:>7} {:>8} {:>6} {:>6} {:>6} {:>7}",
15 "FILE", "DET", "KEPT", "FWHM", "ECC", "SNR", "HFR", "SNR_dB", "SNR_W", "PSF_SIG", "W", "H", "R²", "TRAIL?"
16 );
17 println!("{}", "-".repeat(125));
18
19 for path in &files {
20 let name = path.file_name().unwrap().to_str().unwrap();
21 // Extract short name (frame number)
22 let short = if let Some(pos) = name.rfind('_') {
23 &name[pos + 1..name.len() - 5] // strip .fits
24 } else {
25 &name[..name.len().min(12)]
26 };
27
28 let analyzer = ImageAnalyzer::new();
29 match analyzer.analyze(path) {
30 Ok(r) => {
31 println!(
32 "{:<12} {:>5} {:>5} {:>6.2} {:>6.3} {:>6.1} {:>6.2} {:>7.2} {:>7.3} {:>8.1} {:>6} {:>6} {:>6.3} {:>7}",
33 short,
34 r.stars_detected,
35 r.stars.len(),
36 r.median_fwhm,
37 r.median_eccentricity,
38 r.median_snr,
39 r.median_hfr,
40 r.snr_db,
41 r.snr_weight,
42 r.psf_signal,
43 r.width,
44 r.height,
45 r.trail_r_squared,
46 if r.possibly_trailed { "YES" } else { "no" },
47 );
48 }
49 Err(e) => {
50 println!("{:<12} ERROR: {}", short, e);
51 }
52 }
53 }
54}More examples
3fn analyze_file(label: &str, path: &str) {
4 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
5 println!("{label}");
6 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
7
8 let analyzer = ImageAnalyzer::new();
9 let r = analyzer.analyze(path).unwrap();
10
11 println!(" Stars detected: {}", r.stars_detected);
12 println!(" Stars after filter: {}", r.stars.len());
13 println!(" Median FWHM: {:.3}", r.median_fwhm);
14 println!(" Median eccentricity: {:.3}", r.median_eccentricity);
15 println!(" Median SNR: {:.1}", r.median_snr);
16 println!(" PSF signal: {:.1}", r.psf_signal);
17 println!(" Trail R²: {:.4}", r.trail_r_squared);
18 println!(" Possibly trailed: {}", r.possibly_trailed);
19
20 // Show theta distribution
21 let n = r.stars.len();
22 if n >= 5 {
23 let mut thetas: Vec<f32> = r.stars.iter().map(|s| s.theta.to_degrees()).collect();
24 thetas.sort_by(|a, b| a.total_cmp(b));
25
26 let (sum_cos, sum_sin) = r.stars.iter().fold((0.0f64, 0.0f64), |(sc, ss), s| {
27 let a = 2.0 * s.theta as f64;
28 (sc + a.cos(), ss + a.sin())
29 });
30 let mean_theta = (sum_sin.atan2(sum_cos) * 0.5).to_degrees();
31
32 println!("\n Theta ({} measured stars):", n);
33 println!(" Mean theta: {mean_theta:.1}deg");
34 println!(
35 " min={:.1} p25={:.1} median={:.1} p75={:.1} max={:.1}",
36 thetas[0],
37 thetas[n / 4],
38 thetas[n / 2],
39 thetas[3 * n / 4],
40 thetas[n - 1]
41 );
42 }
43
44 if !r.stars.is_empty() {
45 println!("\n Top 5 stars:");
46 for (i, s) in r.stars.iter().take(5).enumerate() {
47 println!(
48 " #{}: ecc={:.3} theta={:.1}deg fwhm={:.2} peak={:.0}",
49 i + 1,
50 s.eccentricity,
51 s.theta.to_degrees(),
52 s.fwhm,
53 s.peak
54 );
55 }
56 }
57 println!();
58}Sourcepub fn with_detection_sigma(self, sigma: f32) -> Self
pub fn with_detection_sigma(self, sigma: f32) -> Self
Star detection threshold in σ above background.
Sourcepub fn with_min_star_area(self, area: usize) -> Self
pub fn with_min_star_area(self, area: usize) -> Self
Reject connected components with fewer pixels than this (filters hot pixels).
Sourcepub fn with_max_star_area(self, area: usize) -> Self
pub fn with_max_star_area(self, area: usize) -> Self
Reject connected components with more pixels than this (filters galaxies/nebulae).
Sourcepub fn with_saturation_fraction(self, frac: f32) -> Self
pub fn with_saturation_fraction(self, frac: f32) -> Self
Reject stars with peak > fraction × 65535 (saturated).
Sourcepub fn with_max_measure(self, n: usize) -> Self
pub fn with_max_measure(self, n: usize) -> Self
Limit the number of stars measured for PSF fitting. Brightest N detected stars are measured; rest are skipped. Statistics are computed from all measured stars. Default: None (measure all). E.g. 5000 for faster dense fields.
Sourcepub fn with_max_stars(self, n: usize) -> Self
pub fn with_max_stars(self, n: usize) -> Self
Keep only the brightest N stars in the returned result.
Sourcepub fn without_gaussian_fit(self) -> Self
pub fn without_gaussian_fit(self) -> Self
Use fast windowed-moments instead of Gaussian fit for FWHM (less accurate but faster).
Sourcepub fn with_background_mesh(self, cell_size: usize) -> Self
pub fn with_background_mesh(self, cell_size: usize) -> Self
Enable mesh-grid background estimation with given cell size (handles gradients).
Sourcepub fn without_debayer(self) -> Self
pub fn without_debayer(self) -> Self
Skip debayering for OSC images (less accurate but faster).
Sourcepub fn with_trail_threshold(self, threshold: f32) -> Self
pub fn with_trail_threshold(self, threshold: f32) -> Self
Set the R² threshold for trail detection. Images with Rayleigh R² above this are flagged as possibly trailed. Default: 0.5. Lower values are more aggressive (more false positives).
Sourcepub fn with_iterative_background(self, iterations: usize) -> Self
pub fn with_iterative_background(self, iterations: usize) -> Self
Enable iterative source-masked background estimation.
After initial detection, source pixels are masked and background is re-estimated.
Default: 1 (no iteration). Set to 2-3 for images with many bright sources.
Requires with_background_mesh to be set (no-op with global background).
Sourcepub fn with_mrs_noise(self, layers: usize) -> Self
pub fn with_mrs_noise(self, layers: usize) -> Self
Enable MRS (Multiresolution Support) wavelet noise estimation.
Uses à trous B3-spline wavelet to isolate noise from nebulosity/gradients.
layers: number of wavelet layers (1 is sufficient).
Default: 0 (sigma-clipped MAD).
Sourcepub fn with_moffat_beta(self, beta: f32) -> Self
pub fn with_moffat_beta(self, beta: f32) -> Self
Fix Moffat beta to a constant value during PSF fitting. Removes the beta/axis-ratio tradeoff, improving FWHM stability. Typical value: 4.0 (well-corrected optics). Default: None (free beta).
Sourcepub fn with_max_distortion(self, ecc: f32) -> Self
pub fn with_max_distortion(self, ecc: f32) -> Self
Reject detected stars with eccentricity above this threshold before measurement. Pre-filters elongated candidates (optical ghosts, diffraction spikes, edge effects). Typical value: 0.6. Default: None (no filter).
Sourcepub fn with_moffat_fit(self) -> Self
pub fn with_moffat_fit(self) -> Self
Use Moffat PSF fitting instead of Gaussian for more accurate wing modeling. Falls back to Gaussian on non-convergence. Reports per-star β values. This is the default since v0.7.0.
Sourcepub fn without_moffat_fit(self) -> Self
pub fn without_moffat_fit(self) -> Self
Disable Moffat PSF fitting; use Gaussian fit instead.
Sourcepub fn with_thread_pool(self, pool: Arc<ThreadPool>) -> Self
pub fn with_thread_pool(self, pool: Arc<ThreadPool>) -> Self
Use a custom rayon thread pool.
Sourcepub fn analyze<P: AsRef<Path>>(&self, path: P) -> Result<AnalysisResult>
pub fn analyze<P: AsRef<Path>>(&self, path: P) -> Result<AnalysisResult>
Analyze a FITS or XISF image file.
Examples found in repository?
3fn main() {
4 let dir = "tests/no_trails";
5 let mut files: Vec<_> = std::fs::read_dir(dir)
6 .unwrap()
7 .filter_map(|e| e.ok())
8 .filter(|e| e.path().extension().map_or(false, |ext| ext == "fits"))
9 .map(|e| e.path())
10 .collect();
11 files.sort();
12
13 println!(
14 "{:<12} {:>5} {:>5} {:>6} {:>6} {:>6} {:>6} {:>7} {:>7} {:>8} {:>6} {:>6} {:>6} {:>7}",
15 "FILE", "DET", "KEPT", "FWHM", "ECC", "SNR", "HFR", "SNR_dB", "SNR_W", "PSF_SIG", "W", "H", "R²", "TRAIL?"
16 );
17 println!("{}", "-".repeat(125));
18
19 for path in &files {
20 let name = path.file_name().unwrap().to_str().unwrap();
21 // Extract short name (frame number)
22 let short = if let Some(pos) = name.rfind('_') {
23 &name[pos + 1..name.len() - 5] // strip .fits
24 } else {
25 &name[..name.len().min(12)]
26 };
27
28 let analyzer = ImageAnalyzer::new();
29 match analyzer.analyze(path) {
30 Ok(r) => {
31 println!(
32 "{:<12} {:>5} {:>5} {:>6.2} {:>6.3} {:>6.1} {:>6.2} {:>7.2} {:>7.3} {:>8.1} {:>6} {:>6} {:>6.3} {:>7}",
33 short,
34 r.stars_detected,
35 r.stars.len(),
36 r.median_fwhm,
37 r.median_eccentricity,
38 r.median_snr,
39 r.median_hfr,
40 r.snr_db,
41 r.snr_weight,
42 r.psf_signal,
43 r.width,
44 r.height,
45 r.trail_r_squared,
46 if r.possibly_trailed { "YES" } else { "no" },
47 );
48 }
49 Err(e) => {
50 println!("{:<12} ERROR: {}", short, e);
51 }
52 }
53 }
54}More examples
3fn analyze_file(label: &str, path: &str) {
4 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
5 println!("{label}");
6 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
7
8 let analyzer = ImageAnalyzer::new();
9 let r = analyzer.analyze(path).unwrap();
10
11 println!(" Stars detected: {}", r.stars_detected);
12 println!(" Stars after filter: {}", r.stars.len());
13 println!(" Median FWHM: {:.3}", r.median_fwhm);
14 println!(" Median eccentricity: {:.3}", r.median_eccentricity);
15 println!(" Median SNR: {:.1}", r.median_snr);
16 println!(" PSF signal: {:.1}", r.psf_signal);
17 println!(" Trail R²: {:.4}", r.trail_r_squared);
18 println!(" Possibly trailed: {}", r.possibly_trailed);
19
20 // Show theta distribution
21 let n = r.stars.len();
22 if n >= 5 {
23 let mut thetas: Vec<f32> = r.stars.iter().map(|s| s.theta.to_degrees()).collect();
24 thetas.sort_by(|a, b| a.total_cmp(b));
25
26 let (sum_cos, sum_sin) = r.stars.iter().fold((0.0f64, 0.0f64), |(sc, ss), s| {
27 let a = 2.0 * s.theta as f64;
28 (sc + a.cos(), ss + a.sin())
29 });
30 let mean_theta = (sum_sin.atan2(sum_cos) * 0.5).to_degrees();
31
32 println!("\n Theta ({} measured stars):", n);
33 println!(" Mean theta: {mean_theta:.1}deg");
34 println!(
35 " min={:.1} p25={:.1} median={:.1} p75={:.1} max={:.1}",
36 thetas[0],
37 thetas[n / 4],
38 thetas[n / 2],
39 thetas[3 * n / 4],
40 thetas[n - 1]
41 );
42 }
43
44 if !r.stars.is_empty() {
45 println!("\n Top 5 stars:");
46 for (i, s) in r.stars.iter().take(5).enumerate() {
47 println!(
48 " #{}: ecc={:.3} theta={:.1}deg fwhm={:.2} peak={:.0}",
49 i + 1,
50 s.eccentricity,
51 s.theta.to_degrees(),
52 s.fwhm,
53 s.peak
54 );
55 }
56 }
57 println!();
58}Sourcepub fn analyze_data(
&self,
data: &[f32],
width: usize,
height: usize,
channels: usize,
) -> Result<AnalysisResult>
pub fn analyze_data( &self, data: &[f32], width: usize, height: usize, channels: usize, ) -> Result<AnalysisResult>
Analyze pre-loaded f32 pixel data.
data: planar f32 pixel data (for 3-channel: RRRGGGBBB layout).
width: image width.
height: image height.
channels: 1 for mono, 3 for RGB.
Sourcepub fn analyze_raw(
&self,
meta: &ImageMetadata,
pixels: &PixelData,
) -> Result<AnalysisResult>
pub fn analyze_raw( &self, meta: &ImageMetadata, pixels: &PixelData, ) -> Result<AnalysisResult>
Analyze pre-read raw pixel data (skips file I/O).
Accepts ImageMetadata and borrows PixelData, handling u16→f32
conversion and green-channel interpolation for OSC images internally.
Trait Implementations§
Auto Trait Implementations§
impl Freeze for ImageAnalyzer
impl !RefUnwindSafe for ImageAnalyzer
impl Send for ImageAnalyzer
impl Sync for ImageAnalyzer
impl Unpin for ImageAnalyzer
impl UnsafeUnpin for ImageAnalyzer
impl !UnwindSafe for ImageAnalyzer
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more