Source code for madcubapy.operations.maps.noise

from astropy.nddata import CCDData
import astropy.units as u
from copy import copy
from IPython import get_ipython
import matplotlib as mpl
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk
from matplotlib.figure import Figure
import matplotlib.patches as patches
import matplotlib.patheffects as PathEffects
import matplotlib.pyplot as plt
import numpy as np
import platform
import tkinter as tk

from madcubapy.io import MadcubaMap
from madcubapy.visualization import add_wcs_axes
from madcubapy.visualization import insert_colorbar

__all__ = [
    'measure_noise',
]


[docs] def measure_noise( fitsmap, statistic='std', **kwargs): """ Measure the noise (sigma) in a map object by calculating the standard deviation (std) or root mean square (rms) inside several polygons selected by mouse clicks. - Left clicks create polygon vertices. - Right click closes the current polygon, and a subsequent left click starts a new polygon. Parameters ---------- fitsmap : `~madcubapy.io.MadcubaMap` or `~astropy.nddata.CCDData` Map object to analize. statistic : {'std', 'rms'}, optional Statistic to be used as sigma. Defaults to 'std' and can be changed at runtime via GUI buttons. Returns ------- sigma : `~astropy.units.Quantity` Measured noise of the image in the same units as the data array. Other Parameters ---------------- **kwargs Optional map visualization parameters passed to :func:`~madcubapy.visualization.add_wcs_axes`. Notes ----- The function can be aborted by closing the window or pressing 'q'. """ # OS check and kernel check def in_jupyter_notebook(): try: shell = get_ipython() return shell and "IPKernelApp" in shell.config except Exception: return False if in_jupyter_notebook() and platform.system() == 'Darwin': right_click = 2 middle_click = 3 else: right_click = 3 middle_click = 2 if not isinstance(fitsmap, CCDData) and not isinstance(fitsmap, MadcubaMap): raise TypeError(f"Unsupported type: {type(fitsmap)}. " + "Provide a MadcubaMap or CCDData object.") if statistic != 'std' and statistic != 'rms': raise ValueError(f"Invalid input for statistic: '{statistic}'. " + "Accepted values are 'std' or 'rms'.") if ('use_std' not in kwargs and 'vmin' not in kwargs and 'vmax' not in kwargs): kwargs['use_std'] = True # Create a Tkinter window root = tk.Tk() root.title("Measure noise") # Create a Matplotlib figure # If I use mpl.pyplot.figure here and this function is used in a # notebook, an inline plot of the final figure will be shown. fig = Figure(figsize=(7, 6), dpi=100) ax, img = add_wcs_axes(fig, 1, 1, 1, fitsmap=fitsmap, **kwargs) cbar = insert_colorbar(ax) # Prettier plot current_title = ax.get_title() ax.set_title(current_title, fontsize=13, pad=15) ax.coords[0].set_axislabel("RA (ICRS)", fontsize=12) ax.coords[1].set_axislabel("DEC (ICRS)", fontsize=12) ax.coords[0].tick_params(which="major", length=5) ax.coords[0].display_minor_ticks(True) ax.coords[0].set_ticklabel(size=11, exclude_overlapping=True) ax.coords[1].tick_params(which="major", length=5) ax.coords[1].display_minor_ticks(True) ax.coords[1].set_ticklabel(size=11, exclude_overlapping=True, rotation='vertical') cbar.ax.yaxis.label.set_fontsize(12) cbar.ax.tick_params(labelsize=11) # Lists to store the polygons and arrays with pixel data polygon_paths = [] current_polygon = [] inside_pixels = [] sigma = np.nan # Create a callback function for mouse clicks def onclick(event): nonlocal polygon_paths, current_polygon # Left-click to select points if event.button == 1: # Add the clicked point to the current polygon current_polygon.append((event.xdata, event.ydata)) now_polygon = np.array(current_polygon) # Draw a small point at the first clicked point if len(now_polygon) == 1: ax.plot(event.xdata, event.ydata, 'white', marker='o', markersize=5) # Draw lines if len(now_polygon) > 1: ax.plot(now_polygon[-2:, 0], now_polygon[-2:, 1], 'white', lw=2, alpha=0.9) ax.plot(event.xdata, event.ydata, 'white', marker='o', markersize=5) # Refresh the canvas canvas.draw() # Right-click to finalize the current polygon elif event.button == right_click and current_polygon: polygon = np.array(current_polygon) # Draw the last side of the polygon closed_polygon = np.vstack((polygon, polygon[0])) ax.plot(closed_polygon[-2:, 0], closed_polygon[-2:, 1], 'white', lw=2, alpha=0.9) # Draw the polygon on the plot poly = patches.Polygon(xy=polygon, linewidth=2, edgecolor='white', facecolor='white', alpha=0.5) new_poly = copy(poly) ax.add_patch(new_poly) # Get the shape of the CCDData object if fitsmap.header['NAXIS'] == 2: height, width = fitsmap.data.shape fitsmap_data = fitsmap.data elif fitsmap.header['NAXIS'] == 3: freq, height, width = fitsmap.data.shape fitsmap_data = fitsmap.data[0,:,:] elif fitsmap.header['NAXIS'] == 4: freq, stokes, height, width = fitsmap.data.shape fitsmap_data = fitsmap.data[0,0,:,:] # Create a meshgrid of coordinates to create mask x, y = np.meshgrid(range(width), range(height)) points = np.vstack((x.flatten(), y.flatten())).T mask = poly.contains_points(points, radius=0) mask = mask.reshape((height, width)) # Calculate std new_data = copy(fitsmap_data) std = new_data[mask].std(ddof=1) # Paint std inside polygon x_text = polygon.T[0].min() + (polygon.T[0].max() - polygon.T[0].min()) / 2 y_text = polygon.T[1].min() + (polygon.T[1].max() - polygon.T[1].min()) / 2 std_text = ax.text(x_text, y_text, f'{std:.2f}', va='center', ha='center', color='white', fontsize=13) std_text.set_path_effects( [PathEffects.withStroke(linewidth=1.5, foreground="0.5")] ) # Add info on lists polygon_paths.append(polygon) inside_pixels.append(new_data[mask]) # Reset the current polygon current_polygon = [] # Refresh the canvas canvas.draw() # Abort function def _abort(): print("\nFunction aborted") root.quit() root.destroy() # Close function def _quit(): sigma = get_sigma() if not np.isnan(sigma): root.quit() root.destroy() else: root.quit() root.destroy() # Close shortcut def onkeypress(event): if event.key == 'q': _abort() # Clear all drawn and selected polygons def clear_polygons(): nonlocal polygon_paths nonlocal current_polygon nonlocal inside_pixels nonlocal sigma # Reset sigma lists polygon_paths = [] current_polygon = [] inside_pixels = [] sigma = np.nan # Remove sigma painted objects # Remove lines and scatter points for line in ax.lines: line.remove() # Remove patches for patch in ax.patches: patch.remove() # Remove texts for text in ax.texts: text.remove() canvas.draw() # Calculate sigma def get_sigma(): nonlocal sigma all_data = np.array([]) for inside_data in inside_pixels: all_data = np.append(all_data, inside_data) if statistic == 'std': sigma = np.std(all_data, ddof=1) elif statistic == 'rms': sigma = np.sqrt(np.mean(np.square(all_data))) return sigma # Change sigma statistic def change_to_std(): nonlocal statistic statistic = 'std' sigma_button.config(relief=tk.SUNKEN) check_button.config(relief=tk.RAISED) def change_to_rms(): nonlocal statistic statistic = 'rms' sigma_button.config(relief=tk.RAISED) check_button.config(relief=tk.SUNKEN) # Create a Matplotlib canvas embedded within the Tkinter window canvas = FigureCanvasTkAgg(fig, master=root) canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1) # Show toolbar toolbar = NavigationToolbar2Tk(canvas, root) toolbar.update() canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1) # Create a frame for the statistic buttons stat_frame = tk.Frame(root) stat_frame.pack(side=tk.TOP, pady=5) if statistic == 'std': sigma_button = tk.Button(stat_frame, text="STD", relief='sunken', command=change_to_std) sigma_button.pack(side=tk.LEFT, padx=3) check_button = tk.Button(stat_frame, text="RMS", command=change_to_rms) check_button.pack(side=tk.LEFT, padx=3) elif statistic == 'rms': sigma_button = tk.Button(stat_frame, text="STD", command=change_to_std) sigma_button.pack(side=tk.LEFT, padx=3) check_button = tk.Button(stat_frame, text="RMS", relief='sunken', command=change_to_rms) check_button.pack(side=tk.LEFT, padx=3) # Create a frame for main buttons sigma_frame = tk.Frame(root) sigma_frame.pack(side=tk.BOTTOM) clear_button = tk.Button(sigma_frame, text="Clear", command=clear_polygons) clear_button.pack(side=tk.LEFT, padx=3) finish_button_sigma = tk.Button(sigma_frame, text="Finish", command=_quit) finish_button_sigma.pack(side=tk.RIGHT, padx=3) abort_button_sigma = tk.Button(sigma_frame, text="Abort", command=_abort) abort_button_sigma.pack(side=tk.RIGHT, padx=3) # Connect the onclick and onkeypress events to the canvas canvas.mpl_connect('button_press_event', onclick) canvas.mpl_connect('key_press_event', onkeypress) # Start the Tkinter event loop tk.mainloop() return sigma * fitsmap.unit