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} {:>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
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!("  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}
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

Analyze only the brightest N stars.

Source

pub fn without_gaussian_fit(self) -> Self

Use fast windowed-moments instead of Gaussian fit for FWHM (less accurate but faster).

Source

pub fn with_background_mesh(self, cell_size: usize) -> Self

Enable mesh-grid background estimation with given cell size (handles gradients).

Source

pub fn without_debayer(self) -> Self

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

Source

pub fn with_max_eccentricity(self, ecc: f32) -> Self

Reject stars with eccentricity above this threshold (filters elongated objects/trails). Default: 0.5. Set to 1.0 to disable.

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_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} {:>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
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!("  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}
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.