Skip to content

Commit 0400ef8

Browse files
committed
Add ability to create waterfall plots
If the loaded data is an imageseries with 2 to 10 frames per detector, a new button appears in the polar view settings named "Waterfall Plot". If clicked, an azimuthal lineout is computed for every frame in the imageseries, and the results are displayed in an interactive waterfall plot. Since the azimuthal lineout intensities are arbitrarily scaled and offset, the plots can be vertically stacked on top of one another with some spacing between. Clicking and dragging allows one to move a plot up/down. Shift-clicking and dragging also allows the plot to move left/right (which is typically not advised since two theta should not change). Using the mouse wheel while hovering over a lineout causes that lineout to scale up/down in relative intensity. In summary, this allows for easy comparison of the different azimuthal lineouts for each frame in the imageseries. Signed-off-by: Patrick Avery <patrick.avery@kitware.com>
1 parent a1eec8f commit 0400ef8

File tree

7 files changed

+524
-55
lines changed

7 files changed

+524
-55
lines changed

hexrdgui/calibration/polarview.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,6 @@ def __init__(self, instrument, distortion_instrument=None):
5555
# Use an image dict with the panel buffers applied.
5656
# This keeps invalid pixels from bleeding out in the polar view
5757
self.images_dict = HexrdConfig().images_dict
58-
# 0 is a better fill value because it results in fewer nans in
59-
# the final image.
60-
HexrdConfig().apply_panel_buffer_to_images(self.images_dict, 0)
6158

6259
self.warp_dict = {}
6360

@@ -176,8 +173,16 @@ def images_dict(self):
176173

177174
@images_dict.setter
178175
def images_dict(self, v):
176+
# This images_dict sometimes gets modified by external callers,
177+
# such as when a waterfall plot is created. So we need to make
178+
# sure that everything that needs to be updated gets updated
179+
# here.
179180
self._images_dict = v
180181

182+
# 0 is a better fill value because it results in fewer nans in
183+
# the final image.
184+
HexrdConfig().apply_panel_buffer_to_images(self._images_dict, 0)
185+
181186
# Cache the image min and max for later use
182187
self.min = min(x.min() for x in v.values())
183188
self.max = max(x.max() for x in v.values())
@@ -240,13 +245,12 @@ def detector_borders(self, det):
240245
@property
241246
def all_detector_borders(self):
242247
borders = {}
243-
for key in self.images_dict.keys():
248+
for key in self.detectors:
244249
borders[key] = self.detector_borders(key)
245250

246251
return borders
247252

248253
def create_warp_image(self, det):
249-
# lcount = 0
250254
img = self.images_dict[det]
251255
panel = self.detectors[det]
252256

@@ -508,7 +512,7 @@ def warp_all_images(self):
508512
self.reset_cached_distortion_fields()
509513

510514
# Create the warped image for each detector
511-
for det in self.images_dict.keys():
515+
for det in self.detectors:
512516
self.create_warp_image(det)
513517

514518
# Generate the final image
@@ -540,6 +544,9 @@ def update_detectors(self, detectors):
540544
self.generate_image()
541545

542546
def reset_cached_distortion_fields(self):
547+
# These are only reset so that other parts of the code
548+
# will not use them while we are generating new ones.
549+
# They are actually still cached elsewhere.
543550
HexrdConfig().polar_corr_field_polar = None
544551
HexrdConfig().polar_angular_grid = None
545552

hexrdgui/image_canvas.py

Lines changed: 190 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
import sys
55

66
from PySide6.QtCore import QThreadPool, QTimer, Signal, Qt
7-
from PySide6.QtWidgets import QFileDialog, QMessageBox
7+
from PySide6.QtWidgets import QFileDialog, QMessageBox, QProgressDialog
88

9+
from matplotlib.axes import Axes
910
from matplotlib.backends.backend_qtagg import FigureCanvas
1011
from matplotlib.figure import Figure
1112
from matplotlib.lines import Line2D
@@ -20,6 +21,7 @@
2021

2122
from hexrd import distortion as distortion_pkg
2223

24+
from hexrdgui import utils
2325
from hexrdgui.async_worker import AsyncWorker
2426
from hexrdgui.blit_manager import BlitManager
2527
from hexrdgui.calibration.cartesian_plot import cartesian_viewer
@@ -33,7 +35,7 @@
3335
from hexrdgui.masking.create_polar_mask import create_polar_line_data_from_raw
3436
from hexrdgui.masking.mask_manager import MaskManager
3537
from hexrdgui.snip_viewer_dialog import SnipViewerDialog
36-
from hexrdgui import utils
38+
from hexrdgui.waterfall_plot import WaterfallPlotDialog
3739
from hexrdgui.utils.array import split_array
3840
from hexrdgui.utils.conversions import (
3941
angles_to_stereo, cart_to_angles, cart_to_pixels, q_to_tth, tth_to_q,
@@ -76,6 +78,7 @@ def __init__(self, parent=None, image_names=None):
7678
self.raw_view_images_dict = {}
7779
self._mask_boundary_artists = []
7880
self._latest_compute_view_worker = None
81+
self._waterfall_plot_dialog = None
7982

8083
# Track the current mode so that we can more lazily clear on change.
8184
self.mode = None
@@ -1157,55 +1160,10 @@ def finish_show_polar(self, iviewer):
11571160
HexrdConfig().last_unscaled_azimuthal_integral_data = unscaled
11581161

11591162
self.azimuthal_integral_axis = axis
1160-
axis.set_ylabel(r'Azimuthal Average', **self.label_kwargs)
11611163
self.update_azimuthal_plot_overlays()
11621164
self.update_wppf_plot()
11631165

1164-
# Set up formatting for the x-axis
1165-
default_formatter = axis.xaxis.get_major_formatter()
1166-
f = self.format_polar_x_major_ticks
1167-
formatter = PolarXAxisFormatter(default_formatter, f)
1168-
axis.xaxis.set_major_formatter(formatter)
1169-
1170-
axis.yaxis.set_major_locator(AutoLocator())
1171-
axis.yaxis.set_minor_locator(AutoMinorLocator())
1172-
1173-
axis.xaxis.set_major_locator(PolarXAxisTickLocator(self))
1174-
self.axis.xaxis.set_minor_locator(
1175-
PolarXAxisMinorTickLocator(self)
1176-
)
1177-
1178-
# change property of ticks
1179-
axis.tick_params(**self.major_tick_kwargs)
1180-
axis.tick_params(**self.minor_tick_kwargs)
1181-
1182-
# add grid lines parallel to x-axis in azimuthal average
1183-
kwargs = {
1184-
'visible': True,
1185-
'which': 'major',
1186-
'axis': 'y',
1187-
'linewidth': 0.25,
1188-
'linestyle': '-',
1189-
'color': 'k',
1190-
'alpha': 0.75,
1191-
}
1192-
axis.grid(**kwargs)
1193-
1194-
kwargs = {
1195-
'visible': True,
1196-
'which': 'minor',
1197-
'axis': 'y',
1198-
'linewidth': 0.075,
1199-
'linestyle': '--',
1200-
'color': 'k',
1201-
'alpha': 0.9,
1202-
}
1203-
axis.grid(**kwargs)
1204-
1205-
# add grid lines parallel to y-axis
1206-
kwargs['which'] = 'both'
1207-
kwargs['axis'] = 'x'
1208-
axis.grid(**kwargs)
1166+
self._setup_azimuthal_axis(axis)
12091167
else:
12101168
self.update_azimuthal_integral_plot()
12111169
axis = self.azimuthal_integral_axis
@@ -1331,6 +1289,65 @@ def on_beam_energy_modified(self):
13311289
# Update the beam energy on the instrument
13321290
self.iviewer.instr.beam_energy = HexrdConfig().beam_energy
13331291

1292+
def _setup_azimuthal_axis(self, axis: Axes):
1293+
# Set the labels
1294+
axis.set_xlabel(self.polar_xlabel, **self.label_kwargs)
1295+
axis.set_ylabel(r'Azimuthal Average', **self.label_kwargs)
1296+
1297+
# Set up formatting for the x-axis
1298+
# This is important in case "Q" is on the x axis instead
1299+
# of two theta.
1300+
default_formatter = axis.xaxis.get_major_formatter()
1301+
f = self.format_polar_x_major_ticks
1302+
formatter = PolarXAxisFormatter(default_formatter, f)
1303+
axis.xaxis.set_major_formatter(formatter)
1304+
1305+
axis.yaxis.set_major_locator(AutoLocator())
1306+
axis.yaxis.set_minor_locator(AutoMinorLocator())
1307+
1308+
axis.xaxis.set_major_locator(PolarXAxisTickLocator(self))
1309+
self.axis.xaxis.set_minor_locator(
1310+
PolarXAxisMinorTickLocator(self)
1311+
)
1312+
1313+
# change property of ticks
1314+
axis.tick_params(**self.major_tick_kwargs)
1315+
axis.tick_params(**self.minor_tick_kwargs)
1316+
1317+
# Set up the grids
1318+
# These are default kwargs for the grids.
1319+
default_kwargs = {
1320+
'visible': True,
1321+
'linewidth': 0.075,
1322+
'linestyle': '--',
1323+
'color': 'k',
1324+
'alpha': 0.9,
1325+
}
1326+
1327+
# Grid for minor y tickers
1328+
axis.grid(**{
1329+
**default_kwargs,
1330+
'which': 'minor',
1331+
'axis': 'y',
1332+
'linewidth': 0.25,
1333+
'linestyle': '-',
1334+
'alpha': 0.75,
1335+
})
1336+
1337+
# Grid for major y tickers
1338+
axis.grid(**{
1339+
**default_kwargs,
1340+
'which': 'major',
1341+
'axis': 'y',
1342+
})
1343+
1344+
# Grid for all x tickers
1345+
axis.grid(**{
1346+
**default_kwargs,
1347+
'which': 'both',
1348+
'axis': 'x',
1349+
})
1350+
13341351
@property
13351352
def polar_x_axis_type(self):
13361353
return HexrdConfig().polar_x_axis_type
@@ -1510,10 +1527,14 @@ def compute_azimuthal_integral_sum(self, scaled=True):
15101527
pimg = self.scaled_images[0]
15111528
else:
15121529
pimg = self.unscaled_images[0]
1530+
1531+
return self._compute_azimuthal_integral_sum(pimg)
1532+
1533+
def _compute_azimuthal_integral_sum(self, pimg: np.ndarray) -> np.ndarray:
15131534
# !!! NOTE: visible polar masks have already been applied
15141535
# in polarview.py
1515-
masked = np.ma.masked_array(pimg, mask=np.isnan(pimg))
15161536
offset = HexrdConfig().azimuthal_offset
1537+
masked = np.ma.masked_array(pimg, mask=np.isnan(pimg))
15171538
return masked.sum(axis=0) / np.sum(~masked.mask, axis=0) + offset
15181539

15191540
def clear_azimuthal_overlay_artists(self):
@@ -1767,6 +1788,126 @@ def export_current_plot(self, filename):
17671788

17681789
self.iviewer.write_image(filename)
17691790

1791+
def create_waterfall_plot(self):
1792+
if self.mode != ViewType.polar:
1793+
msg = 'Cannot create waterfall plot if we are not in polar mode'
1794+
raise Exception(msg)
1795+
1796+
if not self.iviewer:
1797+
msg = 'Cannot create waterfall plot without an iviewer'
1798+
raise Exception(msg)
1799+
1800+
if self._waterfall_plot_dialog is not None:
1801+
self._waterfall_plot_dialog.hide()
1802+
self._waterfall_plot_dialog = None
1803+
1804+
# Determine the number of lineouts
1805+
num_lineouts = HexrdConfig().imageseries_length
1806+
1807+
# Display a progress dialog indicating that we are
1808+
# generating intensities...
1809+
progress = QProgressDialog(
1810+
'Generating azimuthal lineouts...',
1811+
None,
1812+
0,
1813+
num_lineouts,
1814+
self,
1815+
)
1816+
progress.setWindowTitle('HEXRD')
1817+
progress.setValue(1)
1818+
1819+
# No close button in the corner
1820+
flags = progress.windowFlags()
1821+
progress.setWindowFlags(
1822+
(flags | Qt.CustomizeWindowHint) &
1823+
~Qt.WindowCloseButtonHint
1824+
)
1825+
1826+
self._create_waterfall_progress = progress
1827+
1828+
# Compute azimuthal lineouts in a background thread
1829+
worker = AsyncWorker(self._create_waterfall_lineouts)
1830+
self.thread_pool.start(worker)
1831+
self._latest_compute_view_worker = worker
1832+
1833+
def on_finished():
1834+
progress.reject()
1835+
1836+
# Get the results and close the progress dialog when finished
1837+
worker.signals.result.connect(self._finish_create_waterfall)
1838+
worker.signals.finished.connect(on_finished)
1839+
1840+
progress.exec()
1841+
1842+
def _create_waterfall_lineouts(self) -> list[np.ndarray]:
1843+
progress = self._create_waterfall_progress
1844+
1845+
# Determine the number of lineouts
1846+
num_lineouts = HexrdConfig().imageseries_length
1847+
lineouts = [None] * num_lineouts
1848+
1849+
# We can already compute the lineout for the current frame
1850+
current_idx = HexrdConfig().current_imageseries_idx
1851+
lineouts[current_idx] = self.compute_azimuthal_integral_sum()
1852+
1853+
# Make a deep copy of the iviewer, since we will modify it
1854+
iviewer = copy.deepcopy(self.iviewer)
1855+
1856+
# Now generate the lineouts for the other frames
1857+
for i in range(num_lineouts):
1858+
if i == current_idx:
1859+
# We already generated this one
1860+
continue
1861+
1862+
# Create the new imageseries dict
1863+
HexrdConfig().current_imageseries_idx = i
1864+
try:
1865+
new_images_dict = HexrdConfig().images_dict
1866+
finally:
1867+
# Always restore the previous index
1868+
HexrdConfig().current_imageseries_idx = current_idx
1869+
1870+
# Now force the image dict to change
1871+
iviewer.pv.images_dict = new_images_dict
1872+
1873+
# Generate the new image
1874+
iviewer.pv.warp_all_images()
1875+
1876+
# Grab the new image
1877+
polar_img = iviewer.img
1878+
if HexrdConfig().polar_apply_scaling_to_lineout:
1879+
# Apply the transform
1880+
polar_img = self.transform(polar_img)
1881+
1882+
# Compute the integration
1883+
lineouts[i] = self._compute_azimuthal_integral_sum(polar_img)
1884+
1885+
progress.setValue(progress.value() + 1)
1886+
1887+
return lineouts
1888+
1889+
def _finish_create_waterfall(self, lineouts: list[np.ndarray]):
1890+
# Now create the waterfall plot dialog with the lineouts
1891+
# Create a matplotlib figure and set up everything
1892+
figure = plt.figure()
1893+
ax = figure.add_subplot()
1894+
1895+
# Grab tth
1896+
angular_grid = self.iviewer.angular_grid
1897+
tth = np.degrees(angular_grid[1][0])
1898+
line_data = [(tth, lineout.filled(np.nan)) for lineout in lineouts]
1899+
1900+
# Set up the same azimuthal axes parameters as the polar view
1901+
self._setup_azimuthal_axis(ax)
1902+
1903+
# Disable the tick labels
1904+
ax.set_yticklabels([])
1905+
1906+
# Now create and show the waterfall plot
1907+
dialog = WaterfallPlotDialog(ax, line_data)
1908+
dialog.show()
1909+
self._waterfall_plot_dialog = dialog
1910+
17701911
def export_to_maud(self, filename):
17711912
if self.mode != ViewType.polar:
17721913
msg = 'Must be in polar mode. Cannot export.'

0 commit comments

Comments
 (0)