-
-
Notifications
You must be signed in to change notification settings - Fork 70
/
Copy pathconvenience_functions.py
235 lines (182 loc) · 8.33 KB
/
convenience_functions.py
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
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)