Skip to main content

ImageAnalyzer

Struct ImageAnalyzer 

Source
pub struct ImageAnalyzer { /* private fields */ }
Expand description

Image analyzer with builder pattern.

Implementations§

Source§

impl ImageAnalyzer

Source

pub fn new() -> Self

Examples found in repository?
examples/analyze_batch.rs (line 28)
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} {:>8} {:>8} {:>6} {:>6} {:>6} {:>7}",
15        "FILE", "DET", "KEPT", "FWHM", "ECC", "SNR", "HFR", "SNR_W", "PSF_SIG", "FR_SNR", "W", "H", "R²", "TRAIL?"
16    );
17    println!("{}", "-".repeat(124));
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.3} {:>8.1} {:>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_weight,
41                    r.psf_signal,
42                    r.frame_snr,
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
Hide additional examples
examples/trail_test.rs (line 8)
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!("  Frame SNR:            {:.1}", r.frame_snr);
18    println!("  Trail R²:             {:.4}", r.trail_r_squared);
19    println!("  Possibly trailed:     {}", r.possibly_trailed);
20
21    // Show theta distribution
22    let n = r.stars.len();
23    if n >= 5 {
24        let mut thetas: Vec<f32> = r.stars.iter().map(|s| s.theta.to_degrees()).collect();
25        thetas.sort_by(|a, b| a.total_cmp(b));
26
27        let (sum_cos, sum_sin) = r.stars.iter().fold((0.0f64, 0.0f64), |(sc, ss), s| {
28            let a = 2.0 * s.theta as f64;
29            (sc + a.cos(), ss + a.sin())
30        });
31        let mean_theta = (sum_sin.atan2(sum_cos) * 0.5).to_degrees();
32
33        println!("\n  Theta ({} measured stars):", n);
34        println!("    Mean theta:   {mean_theta:.1}deg");
35        println!(
36            "    min={:.1}  p25={:.1}  median={:.1}  p75={:.1}  max={:.1}",
37            thetas[0],
38            thetas[n / 4],
39            thetas[n / 2],
40            thetas[3 * n / 4],
41            thetas[n - 1]
42        );
43    }
44
45    if !r.stars.is_empty() {
46        println!("\n  Top 5 stars:");
47        for (i, s) in r.stars.iter().take(5).enumerate() {
48            println!(
49                "    #{}: ecc={:.3} theta={:.1}deg fwhm={:.2} peak={:.0}",
50                i + 1,
51                s.eccentricity,
52                s.theta.to_degrees(),
53                s.fwhm,
54                s.peak
55            );
56        }
57    }
58    println!();
59}
Source

pub fn with_detection_sigma(self, sigma: f32) -> Self

Star detection threshold in σ above background.

Source

pub fn with_min_star_area(self, area: usize) -> Self

Reject connected components with fewer pixels than this (filters hot pixels).

Source

pub fn with_max_star_area(self, area: usize) -> Self

Reject connected components with more pixels than this (filters galaxies/nebulae).

Source

pub fn with_saturation_fraction(self, frac: f32) -> Self

Reject stars with peak > fraction × 65535 (saturated).

Source

pub fn with_max_stars(self, n: usize) -> Self

Keep only the brightest N stars in the returned result.

Source

pub fn without_debayer(self) -> Self

Skip debayering for OSC images (less accurate but faster).

Source

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).

Source

pub fn with_optics(self, focal_length_mm: f64, pixel_size_um: f64) -> Self

Set optics parameters for arcsecond-based measurements. focal_length_mm: telescope focal length in millimeters. pixel_size_um: camera pixel size in micrometers. When both are set, FWHM and HFR are reported in arcseconds alongside pixels.

Source

pub fn with_mrs_layers(self, layers: usize) -> Self

Set MRS wavelet noise layers for noise estimation. Default: 0 (fast MAD noise from mesh-grid cell sigmas). Set to 1-6 for MRS wavelet noise (more robust against nebulosity/gradients, ~200ms slower per frame). 4 is the recommended MRS setting.

Source

pub fn with_measure_cap(self, n: usize) -> Self

Max stars to PSF-fit for statistics. Default 2000. Stars are sorted by flux (brightest first) before capping. Set to 0 to measure all detected stars (catalog export mode).

Source

pub fn with_fit_max_iter(self, n: usize) -> Self

LM max iterations for pass-2 measurement fits. Default 25. Calibration pass always uses 50 iterations.

Source

pub fn with_fit_tolerance(self, tol: f64) -> Self

LM convergence tolerance for pass-2 measurement fits. Default 1e-4. Calibration pass always uses 1e-6.

Source

pub fn with_fit_max_rejects(self, n: usize) -> Self

Consecutive LM step rejects before early bailout. Default 5.

Source

pub fn with_thread_pool(self, pool: Arc<ThreadPool>) -> Self

Use a custom rayon thread pool.

Source

pub fn analyze<P: AsRef<Path>>(&self, path: P) -> Result<AnalysisResult>

Analyze a FITS or XISF image file.

Examples found in repository?
examples/analyze_batch.rs (line 29)
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} {:>8} {:>8} {:>6} {:>6} {:>6} {:>7}",
15        "FILE", "DET", "KEPT", "FWHM", "ECC", "SNR", "HFR", "SNR_W", "PSF_SIG", "FR_SNR", "W", "H", "R²", "TRAIL?"
16    );
17    println!("{}", "-".repeat(124));
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.3} {:>8.1} {:>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_weight,
41                    r.psf_signal,
42                    r.frame_snr,
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
Hide additional examples
examples/trail_test.rs (line 9)
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!("  Frame SNR:            {:.1}", r.frame_snr);
18    println!("  Trail R²:             {:.4}", r.trail_r_squared);
19    println!("  Possibly trailed:     {}", r.possibly_trailed);
20
21    // Show theta distribution
22    let n = r.stars.len();
23    if n >= 5 {
24        let mut thetas: Vec<f32> = r.stars.iter().map(|s| s.theta.to_degrees()).collect();
25        thetas.sort_by(|a, b| a.total_cmp(b));
26
27        let (sum_cos, sum_sin) = r.stars.iter().fold((0.0f64, 0.0f64), |(sc, ss), s| {
28            let a = 2.0 * s.theta as f64;
29            (sc + a.cos(), ss + a.sin())
30        });
31        let mean_theta = (sum_sin.atan2(sum_cos) * 0.5).to_degrees();
32
33        println!("\n  Theta ({} measured stars):", n);
34        println!("    Mean theta:   {mean_theta:.1}deg");
35        println!(
36            "    min={:.1}  p25={:.1}  median={:.1}  p75={:.1}  max={:.1}",
37            thetas[0],
38            thetas[n / 4],
39            thetas[n / 2],
40            thetas[3 * n / 4],
41            thetas[n - 1]
42        );
43    }
44
45    if !r.stars.is_empty() {
46        println!("\n  Top 5 stars:");
47        for (i, s) in r.stars.iter().take(5).enumerate() {
48            println!(
49                "    #{}: ecc={:.3} theta={:.1}deg fwhm={:.2} peak={:.0}",
50                i + 1,
51                s.eccentricity,
52                s.theta.to_degrees(),
53                s.fwhm,
54                s.peak
55            );
56        }
57    }
58    println!();
59}
Source

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.

Source

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.

Source

pub fn analyze_batch<P, F>( &self, paths: &[P], concurrency: usize, progress: F, ) -> Vec<(PathBuf, Result<AnalysisResult>)>
where P: AsRef<Path> + Sync, F: Fn(usize, usize, &Path) + Send + Sync,

Analyze multiple images in parallel.

concurrency controls how many frames are analyzed simultaneously. progress is called after each frame completes with (completed, total, path). Returns results in approximate completion order.

Trait Implementations§

Source§

impl Default for ImageAnalyzer

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts 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
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.