use crate::rotation::CoordSystem;
use image::Rgba;
#[derive(Clone, Debug)]
pub struct PixelMask {
pub data: Vec<bool>,
pub fill_color: Option<Rgba<u8>>,
pub coord_system: CoordSystem,
pub nside: i64,
}
impl PixelMask {
pub fn from_vec(
data: Vec<bool>,
nside: i64,
fill_color: Option<Rgba<u8>>,
coord_system: CoordSystem,
) -> Self {
Self {
data,
fill_color,
coord_system,
nside,
}
}
#[inline]
pub fn is_valid(&self, pixel_index: usize) -> bool {
if pixel_index < self.data.len() {
self.data[pixel_index]
} else {
true }
}
pub fn from_value_range(
data: &[f64],
masked_below: Option<f64>,
masked_above: Option<f64>,
nside: i64,
fill_color: Option<Rgba<u8>>,
coord_system: CoordSystem,
) -> Self {
let mask_data: Vec<bool> = data
.iter()
.map(|&val| {
let below_ok = masked_below
.map(|threshold| val >= threshold)
.unwrap_or(true);
let above_ok = masked_above
.map(|threshold| val <= threshold)
.unwrap_or(true);
below_ok && above_ok
})
.collect();
Self::from_vec(mask_data, nside, fill_color, coord_system)
}
pub fn from_fits_file(
path: &str,
fill_color: Option<Rgba<u8>>,
mask_coord: Option<CoordSystem>,
) -> Result<Self, String> {
use fitsrs::hdu::data::bintable::{ColumnId, DataValue};
use fitsrs::{Fits, HDU};
use std::fs::File;
use std::io::BufReader;
let f =
File::open(path).map_err(|e| format!("Failed to open FITS file '{}': {}", path, e))?;
let reader = BufReader::with_capacity(256 * 1024, f);
let mut fits = Fits::from_reader(reader);
let mut mask_data: Vec<f64> = Vec::new();
while let Some(Ok(hdu)) = fits.next() {
if let HDU::XBinaryTable(hdu_ref) = hdu {
let data = fits.get_data(&hdu_ref);
let mut table = data.table_data();
let values = table.select_fields(&[ColumnId::Index(0)]);
for cell in values {
match cell {
DataValue::Double { value, .. } => mask_data.push(value),
DataValue::Float { value, .. } => mask_data.push(value as f64),
DataValue::Integer { value, .. } => mask_data.push(value as f64),
_ => {}
}
}
if !mask_data.is_empty() {
break;
}
}
}
if mask_data.is_empty() {
return Err(format!(
"No mask data found in FITS file '{}'. \
Ensure file contains a binary table with mask in first column.",
path
));
}
let npix = mask_data.len() as f64;
let nside_f = (npix / 12.0).sqrt();
let nside = nside_f as i64;
if (nside * nside * 12) as f64 != npix {
return Err(format!(
"Invalid mask size: {} pixels. Expected 12*nside^2 for some integer nside.",
mask_data.len()
));
}
if !(1..=1024).contains(&nside) {
return Err(format!(
"Calculated NSIDE {} is out of range [1, 1024]",
nside
));
}
let mask_bool: Vec<bool> = mask_data.into_iter().map(|val| val > 0.5).collect();
let coord_system = mask_coord.unwrap_or(CoordSystem::G);
Ok(Self::from_vec(mask_bool, nside, fill_color, coord_system))
}
pub fn needs_transform(&self, data_coord: CoordSystem) -> bool {
self.coord_system != data_coord
}
pub fn warn_coord_mismatch(&self, data_coord: CoordSystem, verbose: bool) -> Option<String> {
if self.needs_transform(data_coord) && verbose {
Some(format!(
"Warning: Mask is in {:?} but data is in {:?}. \
Mask will be applied directly without transformation. \
Results may be incorrect if coordinates don't align!",
self.coord_system, data_coord
))
} else {
None
}
}
}
pub fn parse_maskfill_color(color_str: &str) -> Option<Rgba<u8>> {
match color_str.to_lowercase().as_str() {
"transparent" => None,
"gray" | "grey" => Some(Rgba([128, 128, 128, 255])),
_ => {
let parts: Vec<&str> = color_str.split(',').collect();
if parts.len() == 4
&& let (Ok(r), Ok(g), Ok(b), Ok(a)) = (
parts[0].trim().parse::<u8>(),
parts[1].trim().parse::<u8>(),
parts[2].trim().parse::<u8>(),
parts[3].trim().parse::<u8>(),
)
{
return Some(Rgba([r, g, b, a]));
}
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_value_range_masking() {
let data = vec![0.5, 1.5, 2.5, 3.5];
let mask =
PixelMask::from_value_range(&data, Some(1.0), Some(3.0), 4, None, CoordSystem::G);
assert!(!mask.is_valid(0)); assert!(mask.is_valid(1)); assert!(mask.is_valid(2)); assert!(!mask.is_valid(3)); }
#[test]
fn test_parse_maskfill_color() {
assert_eq!(parse_maskfill_color("transparent"), None);
assert_eq!(
parse_maskfill_color("gray"),
Some(Rgba([128, 128, 128, 255]))
);
assert_eq!(
parse_maskfill_color("255,0,0,255"),
Some(Rgba([255, 0, 0, 255]))
);
}
}