La calibración de las imágenes capturadas es un paso esencial para conseguir unos resultados óptimos, por lo que la calidad de estas imágenes de calibración es crucial.
Con el objeto de determinar si estoy usando la configuración óptima para obtener mis flats he creado un script donde puedo analizar estas imágenes y así determinar su calidad.
He extendido su funcionalidad también a darks, biases y dark-flats para poder ver otros detalles interesantes como son el número de píxeles calientes, ruido MAD o nivel medio ADU
Además, los resultados se pueden ver gráficamente y se guardan en un archivo histórico, de forma que el script puede compararlo con el último análisis y ver así si hay degradación.
Creación y ejecución
import osimport sysimport jsonimport numpy as npimport astropy.io.fits as fitsimport matplotlib.pyplot as pltfrom pathlib import Pathfrom datetime import datetimefrom PyQt6.QtWidgets import (QApplication, QWidget, QPushButton, QTextEdit, QRadioButton, QButtonGroup,QFileDialog, QVBoxLayout, QLabel, QMessageBox, QHBoxLayout, QProgressBar)from PyQt6.QtCore import Qt, QThread, pyqtSignal, QObjectHISTORY_FILE = "calibration_history.json"class AnalysisWorker(QObject):finished = pyqtSignal(dict)progress = pyqtSignal(int)log = pyqtSignal(str)error = pyqtSignal(str)def __init__(self, files, mode, folder_path):super().__init__()self.files = filesself.mode = modeself.folder_path = folder_pathdef run(self):try:means, mads, times, temps = [], [], [], []gains, offsets, hot_pixels_list = [], [], []total = len(self.files)for i, f in enumerate(self.files):with fits.open(f, ignore_missing_end=True) as hdul:data = hdul[0].data.astype(np.float64)hdr = hdul[0].headerm = float(np.mean(data))med = np.median(data)std = np.std(data)means.append(m)mads.append(float(np.median(np.abs(data - med))))times.append(hdr.get("EXPTIME", 0))temps.append(hdr.get("CCD-TEMP", hdr.get("TEMP", 0)))gains.append(hdr.get("GAIN", "N/A"))offsets.append(hdr.get("OFFSET", "N/A"))if self.mode in ["Darks", "Bias"]:hot_pixels = np.sum(data > (m + 5*std))hot_pixels_list.append(hot_pixels)self.progress.emit(int(((i + 1) / total) * 100))results = {"date": datetime.now().strftime("%Y-%m-%d %H:%M"),"mode": self.mode,"folder": self.folder_path,"means": np.array(means),"mads": np.array(mads),"times": np.array(times),"temps": np.array(temps),"gains": gains,"offsets": offsets,"hot_pixels": hot_pixels_list}self.finished.emit(results)except Exception as e:self.error.emit(str(e))class MasterAnalyzer(QWidget):def __init__(self):super().__init__()self.setWindowTitle("Astro-Calibration Suite v1.0")self.resize(900, 800)self.init_ui()def init_ui(self):layout = QVBoxLayout()mode_layout = QHBoxLayout()self.mode_group = QButtonGroup(self)for m in ["Flats", "Darks", "Bias", "Dark-Flats"]:rb = QRadioButton(m)if m == "Flats": rb.setChecked(True)mode_layout.addWidget(rb)self.mode_group.addButton(rb)layout.addLayout(mode_layout)btn_layout = QHBoxLayout()self.btn_run = QPushButton("Analizar Carpeta")self.btn_run.clicked.connect(self.start_analysis)btn_layout.addWidget(self.btn_run)self.btn_plot = QPushButton("Ver Gráfica")self.btn_plot.setEnabled(False)self.btn_plot.clicked.connect(lambda: os.system('xdg-open last_analysis.png &'))btn_layout.addWidget(self.btn_plot)layout.addLayout(btn_layout)self.pbar = QProgressBar()layout.addWidget(self.pbar)self.log_area = QTextEdit()self.log_area.setReadOnly(True)self.log_area.setStyleSheet("background-color: #121212; color: white; font-family: 'DejaVu Sans Mono', monospace; font-size: 13px;")layout.addWidget(self.log_area)self.setLayout(layout)def save_to_history(self, current_data):history = []if os.path.exists(HISTORY_FILE):try:with open(HISTORY_FILE, "r") as f:history = json.load(f)except: history = []entry = {"date": current_data["date"],"mode": current_data["mode"],"folder": current_data["folder"],"mean": float(np.mean(current_data["means"])),"temp": float(np.mean(current_data["temps"])),"hot_pixels": int(np.mean(current_data["hot_pixels"])) if current_data["hot_pixels"] else 0}# Buscar el último registro del mismo tipo para compararlast_match = next((item for item in reversed(history) if item["mode"] == entry["mode"]), None)history.append(entry)with open(HISTORY_FILE, "w") as f:json.dump(history, f, indent=4)return last_matchdef format_row(self, label, value, ref_text, is_ok):color = "#2ecc71" if is_ok else "#e74c3c"icon = "●" if is_ok else "⚠️"safe_ref = ref_text.replace("<", "<").replace(">", ">")return f"""<div style='margin:2px 0;'><span style='color:white;'>{icon} <b>{label:22}</b></span><span style='color:{color}; font-weight: bold;'> {value:>10}</span><span style='color:#888;'> [Ref: {safe_ref}]</span></div>"""def start_analysis(self):folder = QFileDialog.getExistingDirectory(self, "Seleccionar Carpeta")if not folder: returnfiles = sorted([str(p) for p in Path(folder).iterdir() if p.suffix.lower() in (".fit", ".fits")])if not files: returnself.btn_run.setEnabled(False)self.log_area.clear()self.thread = QThread()self.worker = AnalysisWorker(files, self.mode_group.checkedButton().text(), folder)self.worker.moveToThread(self.thread)self.thread.started.connect(self.worker.run)self.worker.progress.connect(self.pbar.setValue)self.worker.finished.connect(self.process_results)self.worker.finished.connect(self.thread.quit)self.thread.start()def process_results(self, data):self.btn_run.setEnabled(True)self.btn_plot.setEnabled(True)# Guardar en JSON y obtener comparaciónlast_data = self.save_to_history(data)# Gráficaplt.figure(figsize=(8, 4))plt.plot(data["means"], marker='o', color='#3498db')plt.title(f"Estabilidad {data['mode']} - {data['date']}")plt.grid(True, alpha=0.2)plt.savefig("last_analysis.png")plt.close()m_val = np.mean(data["means"])std_val = np.std(data["means"])mad_val = np.mean(data["mads"])avg_temp = np.mean(data["temps"])avg_exp = data["times"][0]html = f"<h2 style='color:#3498db; margin-bottom:0;'>📋 INFORME ANALÍTICO: {data['mode'].upper()}</h2>"html += f"<p style='color:#bbb;'>Captura: {data['date']} | <b>Exp: {avg_exp}s</b> | Temp: {avg_temp:.1f}°C</p>"html += f"<p style='color:#777; font-size: 11px;'>Path: {data['folder']}</p>"if last_data:dt = avg_temp - last_data["temp"]diff_color = "#f1c40f" if abs(dt) > 2 else "#888"html += f"<p style='color:{diff_color}; font-size: 12px;'><i>Comparación: Δ Temp vs anterior: {'+' if dt>=0 else ''}{dt:.1f}°C</i></p>"html += "<hr color='#444'>"if data["mode"] == "Flats":html += self.format_row("Promedio ADU", f"{m_val:.1f}", "20.000 - 45.000", 20000 < m_val < 45000)html += self.format_row("Estabilidad (σ)", f"{std_val:.2f}", "< 500.00", std_val < 500)elif data["mode"] == "Darks":hp = int(np.mean(data["hot_pixels"]))html += self.format_row("Nivel Medio (Offset)", f"{m_val:.1f}", "1.500 - 2.500", 1500 < m_val < 2500)html += self.format_row("Hot Pixels", f"{hp}", "< 10.000", hp < 10000)if last_data:d_hp = hp - last_data["hot_pixels"]html += f"<p style='color:#888; margin-left:25px;'>Δ Hot Pixels vs anterior: {'+' if d_hp>=0 else ''}{d_hp}</p>"elif data["mode"] == "Bias":html += self.format_row("Nivel Medio (Offset)", f"{m_val:.1f}", "1.500 - 2.500", 1500 < m_val < 2500)html += self.format_row("Ruido MAD", f"{mad_val:.2f}", "< 20.00", mad_val < 20)elif data["mode"] == "Dark-Flats":html += self.format_row("Nivel Medio ADU", f"{m_val:.1f}", "< 2.500", m_val < 2500)html += "<hr color='#444'>"self.log_area.setHtml(html)if __name__ == "__main__":app = QApplication(sys.argv)window = MasterAnalyzer()window.show()sys.exit(app.exec())
Instrucciones para Windows
- Descargar el último instalador disponible de la versión estable desde aquí: https://www.python.org/downloads/windows/
- En la misma carpeta donde está el archivo script creáis un nuevo archivo ejecutar.bat y lo abrís con un editor de texto
- Copiáis el siguiente texto y guardáis
@echo offif not exist venv (echo Creando entorno virtual...python -m venv venvcall venv\Scripts\activatepip install numpy matplotlib astropy PyQt6 rawpy) else (call venv\Scripts\activate)python CalibrationAnalyzer.pypause
Instrucciones para Linux
- Abrimos un terminal en la carpeta donde tengamos el script e instalamos dependencias
sudo apt install python3-numpy python3-matplotlib python3-scipy python3-pip python3.12-venv libxcb-cursor0
- Creamos un entorno de ejecución para evitar instalar componentes para todo el sistema
python3 -m venv venvsource venv/bin/activate
- Instalamos módulos necesarios
pip install astropy rawpypip install matplotlibpip install PyQt6
source venv/bin/activate
Tanto si lo hacemos por primera vez como en subsecuentes usos, para ejecutar el script:
python3 analyze_flats_gui.py
Uso y funcionamiento
Análisis de flats
En el resumen se indicará el promedio de ADU y la estabilidad de las muestras así como los valores de referencia. Si estuvieran dentro de los límites, el valor aparecerá en verde. Si no, en rojo.
Análisisi de Darks
Análisis de Biases
Análisis de Dark-Flats
Se muestra el nivel medio ADU y en la gráfica los valores de cada muestra
Historial
[{"date": "2026-02-26 09:20","mode": "Flats","folder": "/media/mahg/14b24c2f-9fd8-42cf-9ac6-03d193d21d8d/home/mahg/Pictures/astro/Sh2_274/Flat","mean": 28420.115985328477,"temp": 16.2075,"hot_pixels": 0},{"date": "2026-02-26 09:23","mode": "Flats","folder": "/media/mahg/14b24c2f-9fd8-42cf-9ac6-03d193d21d8d/home/mahg/Pictures/astro/Sh2_274/Flat","mean": 28420.115985328477,"temp": 16.2075,"hot_pixels": 0},{"date": "2026-02-26 09:26","mode": "Darks","folder": "/media/mahg/14b24c2f-9fd8-42cf-9ac6-03d193d21d8d/home/mahg/Pictures/astro/Sh2_274/Dark","mean": 1703.8495726069702,"temp": 17.54,"hot_pixels": 918},{"date": "2026-02-26 09:28","mode": "Bias","folder": "/media/mahg/14b24c2f-9fd8-42cf-9ac6-03d193d21d8d/home/mahg/Pictures/astro/Sh2_274/Bias","mean": 1701.804263334396,"temp": 16.3,"hot_pixels": 12742},{"date": "2026-02-26 09:29","mode": "Dark-Flats","folder": "/media/mahg/14b24c2f-9fd8-42cf-9ac6-03d193d21d8d/home/mahg/Pictures/astro/Sh2_274/DarkFlat","mean": 1701.4514656147694,"temp": 16.035,"hot_pixels": 0}]
No hay comentarios:
Publicar un comentario