From 4d60ac21d7d0c2265d264d7cbc34ffd85f02c3c6 Mon Sep 17 00:00:00 2001 From: Erik Tollerud Date: Sun, 4 Jan 2026 11:42:17 -0700 Subject: [PATCH 1/2] switch to astropy.visualization in ccdproc notebooks --- 09c-Ccdproc/01-ccdproc-overview.ipynb | 37 +++++++++++++++++++----- 09c-Ccdproc/02-image-combination.ipynb | 40 +++++++++++++++++++------- 09c-Ccdproc/ccd_process_solution.py | 7 ++++- 3 files changed, 66 insertions(+), 18 deletions(-) diff --git a/09c-Ccdproc/01-ccdproc-overview.ipynb b/09c-Ccdproc/01-ccdproc-overview.ipynb index a790563..ac587d0 100644 --- a/09c-Ccdproc/01-ccdproc-overview.ipynb +++ b/09c-Ccdproc/01-ccdproc-overview.ipynb @@ -75,7 +75,9 @@ "# %matplotlib widget\n", "\n", "# This function displays images reasonably nicely with minimal effort\n", - "from convenience_functions import show_image" + "#from convenience_functions import show_image\n", + "\n", + "from astropy import visualization" ] }, { @@ -163,7 +165,13 @@ "metadata": {}, "outputs": [], "source": [ - "show_image(raw_stars)" + "#show_image(raw_stars)\n", + "\n", + "plt.figure(figsize=(10,10))\n", + "im, _ = visualization.imshow_norm(raw_stars.data, \n", + " interval=visualization.PercentileInterval(99), \n", + " stretch=visualization.LinearStretch())\n", + "plt.colorbar(im);" ] }, { @@ -255,7 +263,12 @@ "metadata": {}, "outputs": [], "source": [ - "show_image(stars_minus_bias)" + "#show_image(stars_minus_bias)\n", + "plt.figure(figsize=(10,10))\n", + "im, _ = visualization.imshow_norm(stars_minus_bias.data, \n", + " interval=visualization.PercentileInterval(99), \n", + " stretch=visualization.LinearStretch())\n", + "plt.colorbar(im);" ] }, { @@ -309,7 +322,12 @@ "metadata": {}, "outputs": [], "source": [ - "show_image(stars_minus_bias_minus_dark)" + "#show_image(stars_minus_bias_minus_dark)\n", + "plt.figure(figsize=(10,10))\n", + "im, _ = visualization.imshow_norm(stars_minus_bias_minus_dark.data, \n", + " interval=visualization.PercentileInterval(99), \n", + " stretch=visualization.LinearStretch())\n", + "plt.colorbar(im);" ] }, { @@ -359,7 +377,12 @@ "metadata": {}, "outputs": [], "source": [ - "show_image(stars_calibrated)" + "#show_image(stars_calibrated)\n", + "plt.figure(figsize=(10,10))\n", + "im, _ = visualization.imshow_norm(stars_calibrated.data, \n", + " interval=visualization.PercentileInterval(99), \n", + " stretch=visualization.LinearStretch())\n", + "plt.colorbar(im);" ] }, { @@ -536,7 +559,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -550,7 +573,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.14.2" } }, "nbformat": 4, diff --git a/09c-Ccdproc/02-image-combination.ipynb b/09c-Ccdproc/02-image-combination.ipynb index fcba480..a61dc24 100644 --- a/09c-Ccdproc/02-image-combination.ipynb +++ b/09c-Ccdproc/02-image-combination.ipynb @@ -69,8 +69,7 @@ "\n", "# %matplotlib widget\n", "\n", - "# This function display reasonably nicely with minimal effort\n", - "from convenience_functions import show_image" + "from astropy import visualization" ] }, { @@ -251,6 +250,24 @@ "The image on the left below is a single bias image and the image on the right is the combination." ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# fig, axes = plt.subplots(1, 2, sharey=True, tight_layout=True, figsize=(20, 10))\n", + "\n", + "# # Dsiplay the first of the bias images\n", + "# show_image(images_to_combine[0].data, ax=axes[0], fig=fig)\n", + "# axes[0].set_title('Single bias images')\n", + "\n", + "# # Display the combined image -- the perceintile for the low end of the color bar is set\n", + "# # so as to make the range of the data the same in each case.\n", + "# show_image(combined_images, ax=axes[1], fig=fig, percl=99.5)\n", + "# axes[1].set_title('Combined biases')" + ] + }, { "cell_type": "code", "execution_count": null, @@ -259,14 +276,17 @@ "source": [ "fig, axes = plt.subplots(1, 2, sharey=True, tight_layout=True, figsize=(20, 10))\n", "\n", - "# Dsiplay the first of the bias images\n", - "show_image(images_to_combine[0].data, ax=axes[0], fig=fig)\n", + "im1, _ = visualization.imshow_norm(images_to_combine[0].data, ax=axes[0], \n", + " interval=visualization.PercentileInterval(99),\n", + " stretch=visualization.LinearStretch())\n", + "plt.colorbar(im1, ax=axes[0])\n", "axes[0].set_title('Single bias images')\n", "\n", - "# Display the combined image -- the perceintile for the low end of the color bar is set\n", - "# so as to make the range of the data the same in each case.\n", - "show_image(combined_images, ax=axes[1], fig=fig, percl=99.5)\n", - "axes[1].set_title('Combined biases')" + "im2, _ = visualization.imshow_norm(combined_images, ax=axes[1], \n", + " interval=visualization.PercentileInterval(99.5),\n", + " stretch=visualization.LinearStretch())\n", + "plt.colorbar(im2, ax=axes[1])\n", + "axes[1].set_title('Combined biases');" ] }, { @@ -310,7 +330,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -324,7 +344,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.5" + "version": "3.14.2" } }, "nbformat": 4, diff --git a/09c-Ccdproc/ccd_process_solution.py b/09c-Ccdproc/ccd_process_solution.py index bbf379b..053ead3 100644 --- a/09c-Ccdproc/ccd_process_solution.py +++ b/09c-Ccdproc/ccd_process_solution.py @@ -7,4 +7,9 @@ exposure_key='exposure', exposure_unit=u.second, dark_scale=True, master_flat=flat) -show_image(stars_calibrated) # this should look just like the previous version + +plt.figure(figsize=(10,10)) +im, _ = visualization.imshow_norm(stars_calibrated.data, + interval=visualization.PercentileInterval(99), + stretch=visualization.LinearStretch()) +plt.colorbar(im); From 03404f7979264fbbdd1b6388f9fc0e681b406057 Mon Sep 17 00:00:00 2001 From: Erik Tollerud Date: Sun, 4 Jan 2026 11:46:43 -0700 Subject: [PATCH 2/2] clear the convenience_function machinery completely from ccdproc --- 09c-Ccdproc/01-ccdproc-overview.ipynb | 15 +- 09c-Ccdproc/02-image-combination.ipynb | 23 +-- 09c-Ccdproc/convenience_functions.py | 235 ------------------------- 3 files changed, 5 insertions(+), 268 deletions(-) delete mode 100644 09c-Ccdproc/convenience_functions.py diff --git a/09c-Ccdproc/01-ccdproc-overview.ipynb b/09c-Ccdproc/01-ccdproc-overview.ipynb index ac587d0..dd2a2b8 100644 --- a/09c-Ccdproc/01-ccdproc-overview.ipynb +++ b/09c-Ccdproc/01-ccdproc-overview.ipynb @@ -63,6 +63,7 @@ "# Initial imports -- note the import of CCDData from astropy \n", "from astropy.nddata import CCDData\n", "from astropy import units as u\n", + "from astropy import visualization\n", "import ccdproc as ccdp\n", "\n", "from matplotlib import pyplot as plt\n", @@ -72,12 +73,7 @@ "\n", "# Use this instead for interactive plots, but note that you may need \n", "# to install ipympl for it to work\n", - "# %matplotlib widget\n", - "\n", - "# This function displays images reasonably nicely with minimal effort\n", - "#from convenience_functions import show_image\n", - "\n", - "from astropy import visualization" + "# %matplotlib widget" ] }, { @@ -156,7 +152,7 @@ "source": [ "#### Displaying images\n", "\n", - "Images from a telescope are typically very different, in terms of the histogram of values, than other images. The function `show_image`, included along with this notebook, scales and stretches the data to bring out some of the detail that is not visible using matplotlib defaults." + "Images from a telescope are typically very different, in terms of the histogram of values, than other images. The astropy sub-package `astropy.visualization` includes functions that scale and stretch the data to bring out some of the detail that is not visible using matplotlib defaults. We use the simple interface `imshow_norm()`, but see the [astropy.visualization docs](https://docs.astropy.org/en/stable/visualization/index.html) for more on what can be done here." ] }, { @@ -165,8 +161,6 @@ "metadata": {}, "outputs": [], "source": [ - "#show_image(raw_stars)\n", - "\n", "plt.figure(figsize=(10,10))\n", "im, _ = visualization.imshow_norm(raw_stars.data, \n", " interval=visualization.PercentileInterval(99), \n", @@ -263,7 +257,6 @@ "metadata": {}, "outputs": [], "source": [ - "#show_image(stars_minus_bias)\n", "plt.figure(figsize=(10,10))\n", "im, _ = visualization.imshow_norm(stars_minus_bias.data, \n", " interval=visualization.PercentileInterval(99), \n", @@ -322,7 +315,6 @@ "metadata": {}, "outputs": [], "source": [ - "#show_image(stars_minus_bias_minus_dark)\n", "plt.figure(figsize=(10,10))\n", "im, _ = visualization.imshow_norm(stars_minus_bias_minus_dark.data, \n", " interval=visualization.PercentileInterval(99), \n", @@ -377,7 +369,6 @@ "metadata": {}, "outputs": [], "source": [ - "#show_image(stars_calibrated)\n", "plt.figure(figsize=(10,10))\n", "im, _ = visualization.imshow_norm(stars_calibrated.data, \n", " interval=visualization.PercentileInterval(99), \n", diff --git a/09c-Ccdproc/02-image-combination.ipynb b/09c-Ccdproc/02-image-combination.ipynb index a61dc24..4a5534c 100644 --- a/09c-Ccdproc/02-image-combination.ipynb +++ b/09c-Ccdproc/02-image-combination.ipynb @@ -56,6 +56,7 @@ "from astropy.nddata import CCDData\n", "from astropy import units as u\n", "from astropy.stats import mad_std\n", + "from astropy import visualization\n", "\n", "import ccdproc as ccdp\n", "\n", @@ -67,9 +68,7 @@ "# Use this instead for interactive plots, but note that you may need \n", "# to install ipympl for it to work\n", "\n", - "# %matplotlib widget\n", - "\n", - "from astropy import visualization" + "# %matplotlib widget" ] }, { @@ -250,24 +249,6 @@ "The image on the left below is a single bias image and the image on the right is the combination." ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# fig, axes = plt.subplots(1, 2, sharey=True, tight_layout=True, figsize=(20, 10))\n", - "\n", - "# # Dsiplay the first of the bias images\n", - "# show_image(images_to_combine[0].data, ax=axes[0], fig=fig)\n", - "# axes[0].set_title('Single bias images')\n", - "\n", - "# # Display the combined image -- the perceintile for the low end of the color bar is set\n", - "# # so as to make the range of the data the same in each case.\n", - "# show_image(combined_images, ax=axes[1], fig=fig, percl=99.5)\n", - "# axes[1].set_title('Combined biases')" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/09c-Ccdproc/convenience_functions.py b/09c-Ccdproc/convenience_functions.py deleted file mode 100644 index 28e6627..0000000 --- a/09c-Ccdproc/convenience_functions.py +++ /dev/null @@ -1,235 +0,0 @@ -from astropy import visualization as aviz -from astropy.nddata import block_reduce, Cutout2D -from matplotlib import pyplot as plt - - -def show_image(image, - percl=99, percu=None, is_mask=False, - figsize=(10, 10), - cmap='viridis', log=False, clip=True, - show_colorbar=True, show_ticks=True, - fig=None, ax=None, input_ratio=None): - """ - Show an image in matplotlib with some basic astronomically-appropriat stretching. - - Parameters - ---------- - image - The image to show - percl : number - The percentile for the lower edge of the stretch (or both edges if ``percu`` is None) - percu : number or None - The percentile for the upper edge of the stretch (or None to use ``percl`` for both) - figsize : 2-tuple - The size of the matplotlib figure in inches - """ - if percu is None: - percu = percl - percl = 100 - percl - - if (fig is None and ax is not None) or (fig is not None and ax is None): - raise ValueError('Must provide both "fig" and "ax" ' - 'if you provide one of them') - elif fig is None and ax is None: - if figsize is not None: - # Rescale the fig size to match the image dimensions, roughly - image_aspect_ratio = image.shape[0] / image.shape[1] - figsize = (max(figsize) * image_aspect_ratio, max(figsize)) - - fig, ax = plt.subplots(1, 1, figsize=figsize) - - - # To preserve details we should *really* downsample correctly and - # not rely on matplotlib to do it correctly for us (it won't). - - # So, calculate the size of the figure in pixels, block_reduce to - # roughly that,and display the block reduced image. - - # Thanks, https://stackoverflow.com/questions/29702424/how-to-get-matplotlib-figure-size - fig_size_pix = fig.get_size_inches() * fig.dpi - - ratio = (image.shape // fig_size_pix).max() - - if ratio < 1: - ratio = 1 - - ratio = input_ratio or ratio - - reduced_data = block_reduce(image, ratio) - - if not is_mask: - # Divide by the square of the ratio to keep the flux the same in the - # reduced image. We do *not* want to do this for images which are - # masks, since their values should be zero or one. - reduced_data = reduced_data / ratio**2 - - # Of course, now that we have downsampled, the axis limits are changed to - # match the smaller image size. Setting the extent will do the trick to - # change the axis display back to showing the actual extent of the image. - extent = [0, image.shape[1], 0, image.shape[0]] - - if log: - stretch = aviz.LogStretch() - else: - stretch = aviz.LinearStretch() - - norm = aviz.ImageNormalize(reduced_data, - interval=aviz.AsymmetricPercentileInterval(percl, percu), - stretch=stretch, clip=clip) - - if is_mask: - # The image is a mask in which pixels should be zero or one. - # block_reduce may have changed some of the values, so reset here. - reduced_data = reduced_data > 0 - # Set the image scale limits appropriately. - scale_args = dict(vmin=0, vmax=1) - else: - scale_args = dict(norm=norm) - - im = ax.imshow(reduced_data, origin='lower', - cmap=cmap, extent=extent, aspect='equal', **scale_args) - - if show_colorbar: - # I haven't a clue why the fraction and pad arguments below work to make - # the colorbar the same height as the image, but they do....unless the image - # is wider than it is tall. Sticking with this for now anyway... - # Thanks: https://stackoverflow.com/a/26720422/3486425 - fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04) - # In case someone in the future wants to improve this: - # https://joseph-long.com/writing/colorbars/ - # https://stackoverflow.com/a/33505522/3486425 - # https://matplotlib.org/mpl_toolkits/axes_grid/users/overview.html#colorbar-whose-height-or-width-in-sync-with-the-master-axes - - if not show_ticks: - ax.tick_params(labelbottom=False, labelleft=False, labelright=False, labeltop=False) - - -def image_snippet(image, center, width=50, axis=None, fig=None, - is_mask=False, pad_black=False, **kwargs): - """ - Display a subsection of an image about a center. - - Parameters - ---------- - - image : numpy array - The full image from which a section is to be taken. - - center : list-like - The location of the center of the cutout. - - width : int, optional - Width of the cutout, in pixels. - - axis : matplotlib.Axes instance, optional - Axis on which the image should be displayed. - - fig : matplotlib.Figure, optional - Figure on which the image should be displayed. - - is_mask : bool, optional - Set to ``True`` if the image is a mask, i.e. all values are - either zero or one. - - pad_black : bool, optional - If ``True``, pad edges of the image with zeros to fill out width - if the slice is near the edge. - """ - if pad_black: - sub_image = Cutout2D(image, center, width, mode='partial', fill_value=0) - else: - # Return a smaller subimage if extent goes out side image - sub_image = Cutout2D(image, center, width, mode='trim') - show_image(sub_image.data, cmap='gray', ax=axis, fig=fig, - show_colorbar=False, show_ticks=False, is_mask=is_mask, - **kwargs) - - -def _mid(sl): - return (sl.start + sl.stop) // 2 - - -def display_cosmic_rays(cosmic_rays, images, titles=None, - only_display_rays=None): - """ - Display cutouts of the region around each cosmic ray and the other images - passed in. - - Parameters - ---------- - - cosmic_rays : photutils.segmentation.SegmentationImage - The segmented cosmic ray image returned by ``photuils.detect_source``. - - images : list of images - The list of images to be displayed. Each image becomes a column in - the generated plot. The first image must be the cosmic ray mask. - - titles : list of str - Titles to be put above the first row of images. - - only_display_rays : list of int, optional - The number of the cosmic ray(s) to display. The default value, - ``None``, means display them all. The number of the cosmic ray is - its index in ``cosmic_rays``, which is also the number displayed - on the mask. - """ - # Check whether the first image is actually a mask. - - if not ((images[0] == 0) | (images[0] == 1)).all(): - raise ValueError('The first image must be a mask with ' - 'values of zero or one') - - if only_display_rays is None: - n_rows = len(cosmic_rays.slices) - else: - n_rows = len(only_display_rays) - - n_columns = len(images) - - width = 12 - - # The height below is *CRITICAL*. If the aspect ratio of the figure as - # a whole does not allow for square plots then one ends up with a bunch - # of whitespace. The plots here are square by design. - height = width / n_columns * n_rows - fig, axes = plt.subplots(n_rows, n_columns, sharex=False, sharey='row', - figsize=(width, height)) - - # Generate empty titles if none were provided. - if titles is None: - titles = [''] * n_columns - - display_row = 0 - - for row, s in enumerate(cosmic_rays.slices): - if only_display_rays is not None: - if row not in only_display_rays: - # We are not supposed to display this one, so skip it. - continue - - x = _mid(s[1]) - y = _mid(s[0]) - - for column, plot_info in enumerate(zip(images, titles)): - image = plot_info[0] - title = plot_info[1] - is_mask = column == 0 - ax = axes[display_row, column] - image_snippet(image, (x, y), width=80, axis=ax, fig=fig, - is_mask=is_mask) - if is_mask: - ax.annotate('Cosmic ray {}'.format(row), (0.1, 0.9), - xycoords='axes fraction', - color='cyan', fontsize=20) - - if display_row == 0: - # Only set the title if it isn't empty. - if title: - ax.set_title(title) - - display_row = display_row + 1 - - # This choice results in the images close to each other but with - # a small gap. - plt.subplots_adjust(wspace=0.1, hspace=0.05)