-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Visualization utilities #17
Conversation
Codecov ReportPatch coverage:
📣 This organization is not using Codecov’s GitHub App Integration. We recommend you install it so Codecov can continue to function properly for your repositories. Learn more Additional details and impacted files@@ Coverage Diff @@
## main #17 +/- ##
==========================================
+ Coverage 94.03% 94.57% +0.54%
==========================================
Files 9 11 +2
Lines 821 996 +175
==========================================
+ Hits 772 942 +170
- Misses 49 54 +5
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report in Codecov by Sentry. |
Also, I think we need to add |
Your patch plot looks better than mine.
Jupyter? If you drop I have some matplotlib-based tests in https://github.com/svank/wispr_analysis, but they feel really flakey---especially anything that includes text, since apparently font rendering is incredibly platform-specific |
We should make a page in the Jupyter-book docs website for this module. |
regularizepsf/visualize.py
Outdated
patch. | ||
figsize : tuple | ||
If `ax` is not provided, the size of the generated Figure can be set. | ||
uniform_scaling : boolean |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would you not always want uniform_scaling
to be True
?
An alternative way to do this would be to allow specifying a vmin
and vmax
. If they're set to None
then it lets that value vary by different patches. If they're specified, then all the patches use that vmin
and vmax
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this should be specified in the imshow_args
just to avoid redundant variables. An example could show how to do it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the data range should generally be [0, 1] for every patch, since the star cutouts are each normalized, but I had been seeing some of my patches span that range and others only go up to ~0.6 or so, so I thought there might be use cases for either showing how each patch compares to the others, or using the full dynamic range for each individual patch plot.
But I think that variation was because of the way the identified stellar coordinates were being truncated rather than rounded, so the peak-value pixels weren't always aligned. After changing the code to round the stellar coordinates, I think I'm seeing nice [0, 1] ranges for each patch, so this may be unnecessary now. I think we can yank out the uniform_scaling
arg, have the default imshow_args
set vmin=0, vmax=1
, and the user can override those to None
if they really want to.
uniform_scaling : boolean | ||
If True, use the scame colormap scaling for all patches. If False, | ||
normalize each one separately. | ||
imshow_args : dict |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm glad you allow imshow_args
because specifying a custom colormap is important. For the paper, we used a gamma-corrected colormap. We might want to supply that as an option for easy use. This is how I define the paper's colormap:
import matplotlib.colors as clr
a = np.linspace(0, 1, 1000)
r = np.sqrt(a)
g = a
b = np.square(a)
colors = np.stack([r, g, b], axis=-1)
custom = clr.ListedColormap(colors)
We also specified a norm
option for the imshow
command:
axs[i, j].imshow(array_corrector[x, y][117:-117, 117:-117],
cmap=custom,
norm=colors.PowerNorm(gamma=1/2.2, vmin=vmin, vmax=vmax))
It would be good to call out both in the documentation website and in the docstring here the norm
and cmap
parts of this as good practice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I was looking through the default matplotlib colormaps and couldn't figure out which one you were using---this is why! And the gamma correction is probably a good default
regularizepsf/visualize.py
Outdated
xs, ys = [], [] | ||
|
||
for corner, patch in patch_collection.items(): | ||
if psf_size is not None and trim: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we allow a user to specify a custom trim? Maybe they specify a large PSF size for some reason but want the plot just to show the center pixels in some box.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call!
regularizepsf/visualize.py
Outdated
patch = patch[trim:-trim, trim:-trim] | ||
# Since the patches overlap each other, we need to plot each one so it | ||
# only reaches out to the point of overlap. | ||
extent = (corner.y + patch_size/4, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we have different approaches in our plotting here. Since each pixel is sampled 4 times, there are essentially 4 grids of patches. Each grid completely covers the image without overlapping other patches in that grid. You can see how the grids are constructed in this part of the code. In my plots, I took the first quarter of patches and plotted them since there's no overlap. It looks like you're using all the patches. We should discuss which approach is best.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should be possible both to plot a fraction of the patches, and to plot all the patches. The former may be good for cleaner illustrations, while the latter is good for checking that all the patches look reasonable.
One thing I was unsure about in my code was plotting all the patches (from all four grids) together inside one large set of axes labeled with pixel numbers. The axis labels help connect a patch to an image location, but they misrepresent the range of each patch, since I was cramming them all into one big plot despite their sample overlapping regions. I like that your approach removes all the axis labels and adds small borders between each patch, to make their locations more representative than pixel-perfect.
regularizepsf/visualize.py
Outdated
return ax | ||
|
||
|
||
def visualize_patches(patch_collection: PatchCollectionABC, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's the code I used to visualize a before and after set of patches:
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
import itertools
import matplotlib.pylab as pylab
import matplotlib.colors as colors
params = {'axes.titlesize': 15}
pylab.rcParams.update(params)
vmin = 0.02
vmax = 1.0
fig = plt.figure(constrained_layout=True, figsize=(13, 6))
gs = GridSpec(4, 9, figure=fig,
width_ratios=([1]*4) + [0.8] + ([1]*4),
height_ratios=[1]*4)
uncorrected_axs = dict()
for i, j in itertools.product(range(4), range(4)):
uncorrected_axs[i, j] = fig.add_subplot(gs[i, j])
for i in range(16):
row, col = i % 4, i // 4
x = coords[np.array([1, 3, 5, 7])][row]
y = coords[np.array([1, 3, 5, 7])][col]
im = uncorrected_axs[3-row, col].imshow(array_corrector[x, y][117:-117, 117:-117],
origin='lower', cmap=custom,
norm=colors.PowerNorm(gamma=1/2.2, vmin=vmin, vmax=vmax))
uncorrected_axs[3-row, col].set_axis_off()
uncorrected_axs[3-row, col].set_aspect(1)
cax = fig.add_axes([0.479, 0.065, 0.012, 0.85])
#fig.colorbar(im, cax=cax, orientation='vertical', label='log$_{10}$(brightness)')
fig.colorbar(im, cax=cax, orientation='vertical', label='brightness')
corrected_axs = dict()
for i, j in itertools.product(range(4), range(4)):
corrected_axs[i, j] = fig.add_subplot(gs[i, j+5])
for i in range(16):
row, col = i // 4, i % 4
x = coords[np.array([1, 3, 5, 7])][row]
y = coords[np.array([1, 3, 5, 7])][col]
im = corrected_axs[3-row, col].imshow(output[CoordinateIdentifier(None, x, y)][117:-117, 117:-117],
origin='lower', cmap=custom,
norm=colors.PowerNorm(gamma=1/2.2, vmin=vmin, vmax=vmax))
corrected_axs[3-row, col].set_axis_off()
corrected_axs[3-row, col].set_aspect(1)
plt.figtext(0.23, 0.95, 'Uncorrected', ha='center', va='center', fontsize=15)
plt.figtext(0.23+0.54, 0.95, 'Corrected', ha='center', va='center', fontsize=15)
uncorrected_axs[0, 2].set_title("\n")
plt.show()
fig.savefig("/Users/jhughes/Desktop/punch.png", dpi=300)
It would obviously need generalizing if we were to add it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would obviously need generalizing if we were to add it.
fig.savefig("/Users/jhughes/Desktop/punch.png", dpi=300)
I think we can just require the user to log in as jhughes
, no need to generalize
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But I do prefer your patch plots, so I think this code should be the base of what we end up on
regularizepsf/fitter.py
Outdated
@@ -22,8 +22,9 @@ | |||
|
|||
|
|||
class PatchCollectionABC(metaclass=abc.ABCMeta): | |||
def __init__(self, patches: dict[Any, np.ndarray]) -> None: | |||
def __init__(self, patches: dict[Any, np.ndarray], counts: dict[Any, int] = None) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the type hint of counts
should be Optional
since we're setting it to None
.
As for testing, it's not absolutely necessary. I think we can just say in any software reviews that testing plotting is finicky. 88% isn't bad. (Maybe we could boost it a little by checking if there's other code not covered.) It looks like you can specify to not show text when running a Matplotlib test. Maybe that would help? |
384cce7
to
c070ef0
Compare
Just continuing to add more for you to look at when you can, I added a tool to compute and plot the transfer kernel corresponding to each patch. Looks like this: To make this work easily, I went into the Cython code and pulled the regularized inverse into its own function. Something that occurred to me is that for the image-comparison tests I added, the reference images are generated with the changes of #19 and #28 in effect, so the tests won't pass as-is unless this PR is applied on top of those. |
Another update I'm proud of: labeling the axes of these plots with the pixel bounds of each patch. I found this really helpful for matching up these plots to the actual data. It makes the axes a bit more verbose, though, so I have this feature default to off---for large images or small patch sizes, there could be ugly overlapping text. I tried two styles, and should probably choose just one for everything. The second is a bit cleaner but takes up more space. |
This looks good. I think I like the second one better too. |
@svank I went ahead and merged the other PRs. |
I've added docs---I think this PR is in a mergeable state now |
Awesome! Thanks for adding all of this! |
Here's a more substantive PR, involving design decisions---I expect comments and opinions! :)
I've rigged up
find_stars_and_average
to store the number of stars contributing to each patch, and to store those as a.counts
attribute on thePatchCollection
.I added a
visualization
module with two functions---one to plot thatcounts
data:For this plot, at first I was making an array containing the counts and
imshow
ing that, to make a plot like this (color-by-numbers!):But the patches overlap each other and stars contribute to multiple patches, so I thought that sort of plot with space-filling array cells makes it really easy to mis-interpret the plot as marking the number of stars contained within each cell. So instead I did this scatter-plot approach.
The second function plots the patches themselves. (You must also have code to do this---maybe you should substitute yours in here)
Of note, none of this has tests currently