1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
//! Star detection using the SEP (Source Extractor as a Library) C library
use crate::types::{BackgroundMetrics, StarMetrics, StarStats};
use anyhow::{anyhow, Result};
use sep_sys as sep;
use std::ffi::{c_int, CStr};
/// Detect stars using SEP's built-in background estimation and object detection
pub fn detect_stars_with_sep_background(
data: &[f32],
width: usize,
height: usize,
max_stars: Option<usize>,
) -> Result<(StarStats, BackgroundMetrics)> {
unsafe {
// Create a sep_image struct for background estimation
let mut image_data = data.to_vec();
let sep_img = sep::sep_image {
data: image_data.as_mut_ptr() as *const std::ffi::c_void,
noise: std::ptr::null(),
mask: std::ptr::null(),
segmap: std::ptr::null(),
dtype: sep::SEP_TFLOAT as c_int,
ndtype: 0,
mdtype: 0,
sdtype: 0,
segids: std::ptr::null_mut(),
idcounts: std::ptr::null_mut(),
numids: 0,
w: width as i64,
h: height as i64,
noiseval: 0.0,
noise_type: 0,
gain: 1.0,
maskthresh: 0.0,
};
// Set background estimation parameters
let bw = 64; // box width
let bh = 64; // box height
let fw = 3; // filter width
let fh = 3; // filter height
let fthresh = 0.0; // filter threshold
// Create a mutable pointer for the background struct
let mut bkg: *mut sep::sep_bkg = std::ptr::null_mut();
// Estimate background
let status = sep::sep_background(
&sep_img as *const sep::sep_image,
bw,
bh,
fw,
fh,
fthresh,
&mut bkg,
);
if status != 0 {
let mut errbuf = [0i8; 512];
sep::sep_get_errmsg(status, errbuf.as_mut_ptr());
let error_msg = CStr::from_ptr(errbuf.as_ptr()).to_string_lossy();
return Err(anyhow!("SEP background estimation error: {}", error_msg));
}
// Get global background and RMS
let background = sep::sep_bkg_global(bkg);
let rms = sep::sep_bkg_globalrms(bkg);
// Get min and max background values
let mut min_bg = f32::MAX;
let mut max_bg = f32::MIN;
// Calculate background uniformity
let nx = (*bkg).nx;
let ny = (*bkg).ny;
let back = (*bkg).back;
for i in 0..(nx * ny) {
let val = *back.offset(i as isize);
min_bg = min_bg.min(val);
max_bg = max_bg.max(val);
}
// Calculate uniformity as 1 - (max-min)/max
// Higher values (closer to 1) mean more uniform background
let uniformity = if max_bg > 0.0 {
1.0 - (max_bg - min_bg) / max_bg
} else {
1.0
};
// Create background metrics with all values
let bg_metrics =
BackgroundMetrics::with_all_metrics(background, rms, min_bg, max_bg, uniformity);
// Free the background memory
sep::sep_bkg_free(bkg);
// Detect stars using the estimated background and RMS
let star_stats = detect_stars_sep(data, width, height, background, rms, max_stars)?;
Ok((star_stats, bg_metrics))
}
}
/// Detect stars using the SEP library and return detailed measurements for each star.
pub fn detect_stars_sep(
data: &[f32],
width: usize,
height: usize,
background: f32,
std_dev: f32,
max_stars: Option<usize>,
) -> Result<StarStats> {
// Skip processing if image is too small
if width < 3 || height < 3 {
return Ok(StarStats {
count: 0,
median_fwhm: 0.0,
median_eccentricity: 0.0,
fwhm_std_dev: 0.0,
eccentricity_std_dev: 0.0,
median_kron_radius: 0.0,
median_flux: 0.0,
median_snr: 0.0,
median_elongation: 0.0,
flagged_fraction: 0.0,
kron_radius_std_dev: 0.0,
flux_std_dev: 0.0,
snr_std_dev: 0.0,
});
}
// Create a copy of the data as f32 (SEP requires contiguous memory)
let mut image_data = data.to_vec();
unsafe {
// Create a sep_image struct
let sep_img = sep::sep_image {
data: image_data.as_mut_ptr() as *const std::ffi::c_void,
noise: std::ptr::null(),
mask: std::ptr::null(),
segmap: std::ptr::null(),
dtype: sep::SEP_TFLOAT as c_int,
ndtype: 0,
mdtype: 0,
sdtype: 0,
segids: std::ptr::null_mut(),
idcounts: std::ptr::null_mut(),
numids: 0,
w: width as i64,
h: height as i64,
noiseval: std_dev as f64,
noise_type: sep::SEP_NOISE_STDDEV,
gain: 1.0,
maskthresh: 0.0,
};
// Set threshold to 3 sigma above background
let thresh = background + 3.0 * std_dev;
// Increase SEP's internal extraction pixel stack for large/crowded images.
// Default is 300_000, which can underflow for modern high-resolution frames.
let required_pixstack = width.saturating_mul(height).clamp(300_000, 20_000_000);
let current_pixstack = sep::sep_get_extract_pixstack();
if required_pixstack > current_pixstack {
sep::sep_set_extract_pixstack(required_pixstack);
}
sep::sep_set_sub_object_limit(100_000);
// Create pointers for the catalog
let mut catalog: *mut sep::sep_catalog = std::ptr::null_mut();
// Call SEP to extract objects
let status = sep::sep_extract(
&sep_img as *const sep::sep_image,
thresh,
sep::SEP_THRESH_ABS as c_int,
5, // Minimum area in pixels
std::ptr::null(), // No convolution filter
0, // No convolution width
0, // No convolution height
sep::SEP_FILTER_CONV as c_int,
32, // Deblend thresholds
0.005, // Deblend contrast
1, // Clean flag
1.0, // Clean parameter
&mut catalog,
);
// Check for errors
if status != 0 {
let mut errbuf = [0i8; 512];
sep::sep_get_errmsg(status, errbuf.as_mut_ptr());
let error_msg = CStr::from_ptr(errbuf.as_ptr()).to_string_lossy();
return Err(anyhow!("SEP error: {}", error_msg));
}
// Convert SEP catalog to Vec<StarMetrics>
let nobj = (*catalog).nobj as usize;
let mut stars = Vec::with_capacity(nobj);
for i in 0..nobj {
// Get pointers to arrays
let x = *(*catalog).x.add(i);
let y = *(*catalog).y.add(i);
let a = *(*catalog).a.add(i);
let b = *(*catalog).b.add(i);
let theta = *(*catalog).theta.add(i);
let flux = *(*catalog).flux.add(i);
let peak = *(*catalog).peak.add(i);
// Extract additional metrics from SEP catalog
let npix = (*catalog).npix.add(i) as usize;
let flag = *(*catalog).flag.add(i) as u8;
// Calculate derived metrics
let elongation = if a > 0.0 && b > 0.0 { a / b } else { 1.0 };
// Calculate Kron radius (radius containing 50% of flux)
let mut kron_radius = 0.0;
let mut krflag: i16 = 0;
// Call sep_kron_radius to get the Kron radius
// Note: SEP expects f64 for coordinates and dimensions
let kr_status = sep::sep_kron_radius(
&sep_img as *const sep::sep_image,
x,
y, // Object position (already f64)
a as f64,
b as f64, // Semi-major and semi-minor axes (convert f32 to f64)
theta as f64, // Position angle (convert f32 to f64)
6.0, // Number of Kron radii for measurement (typically 2.5 or 6.0)
0, // Flags (0 = default)
&mut kron_radius as *mut f32 as *mut f64, // Output Kron radius
&mut krflag, // Output flag
);
if kr_status != 0 {
// If there's an error, use a default value
kron_radius = 0.0;
}
// Calculate flux within elliptical aperture (AUTO flux)
let mut flux_auto = flux;
let mut fluxerr_auto = 0.0;
let _seflag = 0;
if kron_radius > 0.0 {
// Use Kron radius for aperture measurement (typically 2.5 * kron_radius)
let kr_scale = 2.5;
let auto_a = kr_scale * kron_radius * a;
let auto_b = kr_scale * kron_radius * b;
// Call sep_sum_ellipse to get the flux within the aperture
let mut flux_auto_f64 = flux_auto as f64;
let mut fluxerr_auto_f64 = 0.0;
let mut seflag: i16 = 0;
let se_status = sep::sep_sum_ellipse(
&sep_img as *const sep::sep_image,
x,
y, // Object position (already f64)
auto_a as f64,
auto_b as f64, // Scaled semi-major and semi-minor axes
theta as f64, // Position angle
0.0, // No inner radius (full ellipse)
0, // Subpixel sampling (0 = default)
0, // Error type (0 = default)
0, // Aperture correction (0 = none)
&mut flux_auto_f64, // Output flux
&mut fluxerr_auto_f64, // Output flux error
&mut 0.0, // Output area (not used)
&mut seflag, // Output flag
);
// Convert back to f32 for our struct
flux_auto = flux_auto_f64 as f32;
fluxerr_auto = fluxerr_auto_f64 as f32;
if se_status != 0 {
// If there's an error, use the isophotal flux
flux_auto = flux;
fluxerr_auto = 0.0;
}
}
let mut star = StarMetrics {
x,
y,
flux,
peak,
a,
b,
theta,
eccentricity: 0.0,
fwhm: 0.0,
kron_radius,
flux_auto,
fluxerr_auto,
npix,
elongation,
flag,
};
// Calculate derived metrics
star.calc_eccentricity();
star.calc_fwhm();
stars.push(star);
}
// Free the memory allocated by SEP
if !catalog.is_null() {
sep::sep_catalog_free(catalog);
}
// Check if we found any stars
if stars.is_empty() {
return Ok(StarStats {
count: 0,
median_fwhm: 0.0,
median_eccentricity: 0.0,
fwhm_std_dev: 0.0,
eccentricity_std_dev: 0.0,
median_kron_radius: 0.0,
median_flux: 0.0,
median_snr: 0.0,
median_elongation: 0.0,
flagged_fraction: 0.0,
kron_radius_std_dev: 0.0,
flux_std_dev: 0.0,
snr_std_dev: 0.0,
});
}
// Calculate aggregate statistics
let stats = StarStats::from_stars(&stars, max_stars);
Ok(stats)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore] // Ignore this test as it requires the SEP library to be properly initialized
fn test_detect_stars_sep() {
let (w, h) = (20, 20);
let mut data = vec![0.0; w * h];
// Add a bright star in the center
data[10 * w + 10] = 100.0;
// Add some fainter stars
data[5 * w + 5] = 50.0;
data[15 * w + 15] = 50.0;
// Test detection with background estimation
let result = detect_stars_with_sep_background(&data, w, h, None);
assert!(result.is_ok());
let (stats, bg_metrics) = result.unwrap();
assert!(stats.count > 0, "Should detect at least one star");
assert!(stats.median_fwhm > 0.0, "FWHM should be positive");
assert!(stats.median_eccentricity >= 0.0 && stats.median_eccentricity <= 1.0);
assert!(
bg_metrics.rms >= 0.0,
"Background RMS should be non-negative"
);
assert!(
bg_metrics.uniformity >= 0.0 && bg_metrics.uniformity <= 1.0,
"Uniformity should be between 0 and 1"
);
}
}