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_mrs_layers(self, layers: usize) -> Self

Set MRS wavelet noise layers. Uses à trous B3-spline wavelet to isolate noise from nebulosity/gradients. Default: 4.

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.

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.