diff --git a/TESTS/unitTests.py b/TESTS/unitTests.py index e12a9b4..7c34eef 100755 --- a/TESTS/unitTests.py +++ b/TESTS/unitTests.py @@ -8,6 +8,8 @@ import matplotlib.pyplot as plt import pyrtools as pt +from pyrtools.pyramids.pyramid import Pyramid +from pyrtools.tools.display import colormap_range import scipy.io import os @@ -1599,7 +1601,158 @@ def test_pyrshow_2d_shape_err(self): with self.assertRaises(ValueError): pt.pyrshow(pyr.pyr_coeffs) - +class TestVrange(unittest.TestCase): + + def _get_clims(self, fig): + """get vmin vmax for each image in fig as list of tuples (vmin, vmax)""" + return [ax.images[0].get_clim() for ax in fig.axes if ax.images] + + def _get_title_clims(self, fig): + """get vmin vmax for each image in fig as list of tuples (vmin, vmax)""" + clims = [] + for ax in fig.axes: + title = ax.get_title() + vmin, vmax = title.split('[')[1].split(']')[0].split(',') + clims.append((float(vmin.strip()), float(vmax.strip()))) + return clims + + def _get_images(self): + # define test images such that each image has a different range of values, + # so we can test that the correct vrange is applied to each one + images = [np.arange(4 * i, 4 * i + 4, dtype=float).reshape(2, 2) for i in range(4)] + return images + + def _imshow(self, vrange): + return pt.imshow(self._get_images(), vrange=vrange, zoom=1, col_wrap=2) + + def _expected_clims(self, vrange): + clims, _ = colormap_range(image=self._get_images(), contains_rgb= [False]*len(self._get_images()), vrange=vrange, cmap=None, n_cols=2) + return clims + + def test_global_vrange_all_images_share_clim(self): + for mode in range(4): + with self.subTest(mode=mode): + clims = self._get_clims(self._imshow(f"auto{mode}")) + self.assertTrue(all(c == clims[0] for c in clims)) + + def test_global_vrange_vmin(self): + for mode in range(4): + for img_idx in range(4): + with self.subTest(mode=mode, img_idx=img_idx): + vmin, _ = self._get_clims(self._imshow(f"auto{mode}"))[img_idx] + exp_vmin, _ = self._expected_clims(f"auto{mode}")[img_idx] + self.assertTrue(np.isclose(vmin, exp_vmin, atol=1e-6)) + + def test_global_vrange_vmax(self): + for mode in range(4): + for img_idx in range(4): + with self.subTest(mode=mode, img_idx=img_idx): + _, vmax = self._get_clims(self._imshow(f"auto{mode}"))[img_idx] + _, exp_vmax = self._expected_clims(f"auto{mode}")[img_idx] + self.assertTrue(np.isclose(vmax, exp_vmax, atol=1e-6)) + + def test_global_vrange_title_matches_clim(self): + for mode in range(4): + for img_idx in range(4): + with self.subTest(mode=mode, img_idx=img_idx): + fig = self._imshow(f"auto{mode}") + clim_vmin, clim_vmax = self._get_clims(fig)[img_idx] + title_vmin, title_vmax = self._get_title_clims(fig)[img_idx] + self.assertEqual("{:.1e}".format(clim_vmin), "{:.1e}".format(title_vmin)) + self.assertEqual("{:.1e}".format(clim_vmax), "{:.1e}".format(title_vmax)) + + def test_row_vrange_same_row_shares_clim(self): + for mode in range(4): + with self.subTest(mode=mode): + clims = self._get_clims(self._imshow(f"auto{mode}row")) + self.assertEqual(clims[0], clims[1], "row 0 images differ") + self.assertEqual(clims[2], clims[3], "row 1 images differ") + + def test_row_vrange_vmin(self): + for mode in range(4): + for img_idx in range(4): + with self.subTest(mode=mode, img_idx=img_idx): + vmin, _ = self._get_clims(self._imshow(f"auto{mode}row"))[img_idx] + exp_vmin, _ = self._expected_clims(f"auto{mode}row")[img_idx] + self.assertTrue(np.isclose(vmin, exp_vmin, atol=1e-6)) + + def test_row_vrange_vmax(self): + for mode in range(4): + for img_idx in range(4): + with self.subTest(mode=mode, img_idx=img_idx): + _, vmax = self._get_clims(self._imshow(f"auto{mode}row"))[img_idx] + _, exp_vmax = self._expected_clims(f"auto{mode}row")[img_idx] + self.assertTrue(np.isclose(vmax, exp_vmax, atol=1e-6)) + + def test_row_vrange_title_matches_clim(self): + for mode in range(4): + for img_idx in range(4): + with self.subTest(mode=mode, img_idx=img_idx): + fig = self._imshow(f"auto{mode}row") + clim_vmin, clim_vmax = self._get_clims(fig)[img_idx] + title_vmin, title_vmax = self._get_title_clims(fig)[img_idx] + self.assertEqual("{:.1e}".format(clim_vmin), "{:.1e}".format(title_vmin)) + self.assertEqual("{:.1e}".format(clim_vmax), "{:.1e}".format(title_vmax)) + + def test_col_vrange_same_col_shares_clim(self): + for mode in range(4): + with self.subTest(mode=mode): + clims = self._get_clims(self._imshow(f"auto{mode}col")) + self.assertEqual(clims[0], clims[2], "col 0 images differ") + self.assertEqual(clims[1], clims[3], "col 1 images differ") + + def test_col_vrange_vmin(self): + for mode in range(4): + for img_idx in range(4): + with self.subTest(mode=mode, img_idx=img_idx): + vmin, _ = self._get_clims(self._imshow(f"auto{mode}col"))[img_idx] + exp_vmin, _ = self._expected_clims(f"auto{mode}col")[img_idx] + self.assertTrue(np.isclose(vmin, exp_vmin, atol=1e-6)) + + def test_col_vrange_vmax(self): + for mode in range(4): + for img_idx in range(4): + with self.subTest(mode=mode, img_idx=img_idx): + _, vmax = self._get_clims(self._imshow(f"auto{mode}col"))[img_idx] + _, exp_vmax = self._expected_clims(f"auto{mode}col")[img_idx] + self.assertTrue(np.isclose(vmax, exp_vmax, atol=1e-6)) + + def test_col_vrange_title_matches_clim(self): + for mode in range(4): + for img_idx in range(4): + with self.subTest(mode=mode, img_idx=img_idx): + fig = self._imshow(f"auto{mode}col") + clim_vmin, clim_vmax = self._get_clims(fig)[img_idx] + title_vmin, title_vmax = self._get_title_clims(fig)[img_idx] + self.assertEqual("{:.1e}".format(clim_vmin), "{:.1e}".format(title_vmin)) + self.assertEqual("{:.1e}".format(clim_vmax), "{:.1e}".format(title_vmax)) + + def test_indep_vrange_vmin(self): + for mode in range(4): + for img_idx in range(4): + with self.subTest(mode=mode, img_idx=img_idx): + vmin, _ = self._get_clims(self._imshow(f"indep{mode}"))[img_idx] + exp_vmin, _ = self._expected_clims(f"indep{mode}")[img_idx] + self.assertTrue(np.isclose(vmin, exp_vmin, atol=1e-6)) + + def test_indep_vrange_vmax(self): + for mode in range(4): + for img_idx in range(4): + with self.subTest(mode=mode, img_idx=img_idx): + _, vmax = self._get_clims(self._imshow(f"indep{mode}"))[img_idx] + _, exp_vmax = self._expected_clims(f"indep{mode}")[img_idx] + self.assertTrue(np.isclose(vmax, exp_vmax, atol=1e-6)) + + def test_indep_vrange_title_matches_clim(self): + for mode in range(4): + for img_idx in range(4): + with self.subTest(mode=mode, img_idx=img_idx): + fig = self._imshow(f"indep{mode}") + clim_vmin, clim_vmax = self._get_clims(fig)[img_idx] + title_vmin, title_vmax = self._get_title_clims(fig)[img_idx] + self.assertEqual("{:.1e}".format(clim_vmin), "{:.1e}".format(title_vmin)) + self.assertEqual("{:.1e}".format(clim_vmax), "{:.1e}".format(title_vmax)) + def main(): unittest.main() diff --git a/src/pyrtools/tools/display.py b/src/pyrtools/tools/display.py index 534fa41..35e1b6e 100644 --- a/src/pyrtools/tools/display.py +++ b/src/pyrtools/tools/display.py @@ -1,3 +1,4 @@ +import math import warnings import numpy as np import matplotlib.pyplot as plt @@ -225,7 +226,7 @@ def reshape_axis(ax, axis_size_pix): return ax -def colormap_range(image, contains_rgb, vrange='indep1', cmap=None): +def colormap_range(image, contains_rgb, vrange='indep1', cmap=None, n_cols = None): """Find the appropriate ranges for colormaps of provided images Arguments @@ -239,10 +240,13 @@ def colormap_range(image, contains_rgb, vrange='indep1', cmap=None): contains_rgb : list List of bools specifying whether each of the signals in image are RGB or not. - vrange : `tuple` or `str` + vrange : `tuple` or `list` or `str` If a 2-tuple, specifies the image values vmin/vmax that are mapped to (ie. clipped to) the minimum and maximum value of the colormap, respectively. + If a list of 2-tuples, the length of number of images, each image has an + independent vmin/vmax that are mapped to the minimum/maximum value of + the colormap, respectively. If a string: * `'auto0'`: all images have same vmin/vmax, which have the same absolute value, and come from the minimum or maximum across @@ -256,6 +260,19 @@ def colormap_range(image, contains_rgb, vrange='indep1', cmap=None): the display intensity range. For example: vmin is the 10th percentile image value minus 1/8 times the difference between the 90th and 10th percentile + * `'auto[X]row'`: each row of the figure has the same vmin/vmax, which are + computed using the auto[X] methods described above Eg. + `'auto1row'`, `'auto2row'`, or `'auto3row'`Ie. min/max, + mean minus/plus 2 std dev, or percentile statistics are + computed across all images in a given row, and those + values are used as the vmin/vmax for all images in that row. + High pass and low pass residuals have independent vmin/vmax + based on min/max of the residual image itself. + * `'auto[X]col'`: each column of the figure has the same vmin/vmax, which are + computed using the auto[X] methods described above Eg. + `'auto1col'`, `'auto2col'`, or `'auto3col'`. + * `'auto[X]colcomplex'`: vmin and vmax for each column is computed across both + the realand imaginary parts of all images in that column. * `'indep0'`: each image has an independent vmin/vmax, which have the same absolute value, which comes from either their minimum or maximum value, whichever has the larger absolute value. @@ -266,6 +283,10 @@ def colormap_range(image, contains_rgb, vrange='indep1', cmap=None): * `'indep3'`: each image has an independent vmin/vmax, chosen so that the 10th/90th percentile values map to the 10th/90th percentile intensities. + cmap : matplotlib colormap, optional + colormap to use when showing these images. If None, will pick RdBu_r if vrange is some variant of auto0 or indep0, else will pick gray. + n_cols : `int` + number of columns in the figure. Returns ------- @@ -274,6 +295,13 @@ def colormap_range(image, contains_rgb, vrange='indep1', cmap=None): for each image. """ if isinstance(vrange, str): + + if 'indep' in vrange and 'row' in vrange: + raise ValueError("indep and row cannot be used together in vrange. Use either indep or auto[x]row") + if 'indep' in vrange and 'col' in vrange: + raise ValueError("indep and col cannot be used together in vrange. Use either indep or " \ + "auto[x]col or auto[x]colcomplex") + if vrange[:4] == 'auto': # flatimg is one long 1d array, which enables the min, max, mean, std, and # percentile calls to operate on the values from each of the images simultaneously. @@ -282,8 +310,28 @@ def colormap_range(image, contains_rgb, vrange='indep1', cmap=None): # for more. try: flatimg = np.concatenate([i.flatten() for i, rgb in zip(image, contains_rgb) - if not rgb]).flatten() - if vrange == 'auto0': + if not rgb]).flatten() + if 'row' in vrange: + assert n_cols is not None, "n_cols must be provided when using row-wise vrange (e.g. 'auto1row')" + vrange_tmp = [] + for i in range(math.ceil(len(image) / n_cols)): + vr, _ = colormap_range( + image[n_cols * i : n_cols * (i + 1)], contains_rgb, vrange.split('row')[0]) + vrange_tmp.extend(vr) + elif 'col' in vrange: + if 'complex' in vrange: + imgs_formatted = [np.concatenate([image[ii+1], image[ii+1]]) for ii in range(0, len(image), 2)] + # Divide by 2 because we are grouping complex images into pairs + vrange_tmp, cmap = colormap_range(imgs_formatted, contains_rgb, vrange=vrange.split('complex')[0], n_cols=n_cols//2) + vrange_tmp = [v for v in vrange_tmp for _ in range(2)] + else: + vrange_tmp = [None] * len(image) + for j in range(n_cols): + col_images = [image[i] for i in range(j, len(image), n_cols)] + vr, _ = colormap_range(col_images, contains_rgb, vrange.split('col')[0]) + for k, i in enumerate(range(j, len(image), n_cols)): + vrange_tmp[i] = vr[k] + elif vrange == 'auto0': M = np.nanmax([np.abs(np.nanmin(flatimg)), np.abs(np.nanmax(flatimg))]) vrange_tmp = [-M, M] elif vrange == 'auto1' or vrange == 'auto': @@ -300,19 +348,20 @@ def colormap_range(image, contains_rgb, vrange='indep1', cmap=None): # because the vrange doesn't depend on the computed values above, it # must be either [0, 1] for floats or [0, 255] for ints pass - # make sure to return as many ranges as there are images vrange_list = [] - for img, rgb in zip(image, contains_rgb): - if not rgb: - vrange_list.append(vrange_tmp) - elif np.issubdtype(img.dtype, np.floating): - # all RGB float images use vrange 0, 1 - vrange_list.append([0, 1]) - else: - # all RGB int images use vrange 0, 255 - vrange_list.append([0, 255]) - + if 'row' not in vrange and 'col' not in vrange: + for img, rgb in zip(image, contains_rgb): + if not rgb: + vrange_list.append(vrange_tmp) + elif np.issubdtype(img.dtype, np.floating): + # all RGB float images use vrange 0, 1 + vrange_list.append([0, 1]) + else: + # all RGB int images use vrange 0, 255 + vrange_list.append([0, 255]) + else: + vrange_list = vrange_tmp elif vrange[:5] == 'indep': # independent vrange from recursive calls of this function per image vrange_list = [colormap_range([im], [rgb], vrange.replace('indep', 'auto') @@ -322,14 +371,17 @@ def colormap_range(image, contains_rgb, vrange='indep1', cmap=None): vrange_list, _ = colormap_range(image, contains_rgb, 'auto1') warnings.warn('Unknown vrange argument, using auto1 instead') else: - # two numbers were passed, either as a list or tuple - if len(vrange) != 2: + # either a single 2-tuple was passed or a list of 2-tuples one for each image was passed. + if len(vrange) == 2: + vrange_list = [tuple(vrange)] * len(image) + elif len(vrange) == len(image): + # explicitly cast as tuple in case of list of lists + vrange_list = [tuple(v) for v in vrange] + else: raise Exception("If you're passing numbers to vrange," - "there must be 2 of them!") - vrange_list = [tuple(vrange)] * len(image) - + "there must be a single 2-tuple or as many as there are images!") if cmap is None: - if '0' in vrange: + if isinstance(vrange, str) and '0' in vrange: cmap = cm.RdBu_r else: cmap = cm.gray @@ -645,7 +697,7 @@ def _setup_figure(ax, col_wrap, image, zoom, max_shape, vert_pct): else: fig = ax.figure axes = [reshape_axis(ax, zoom * max_shape)] - return fig, axes + return fig, axes, n_cols def imshow(image, vrange='indep1', zoom=1, title='', col_wrap=None, ax=None, @@ -663,37 +715,66 @@ def imshow(image, vrange='indep1', zoom=1, title='', col_wrap=None, ax=None, `(n,h,w)` for multiple grayscale images). all images will be automatically rescaled so they're displayed at the same size. thus, their sizes must be scalar multiples of each other. - vrange : `tuple` or `str` + vrange : `tuple` or `list` or `str` + + .. attention:: + this only affects the behavior for grayscale images. RGB images + will always be displayed with vrange [0, 1] (for floats) or [0, 255] + (for ints), because of how matplotlib handles them. + If a 2-tuple, specifies the image values vmin/vmax that are mapped to - the minimum and maximum value of the colormap, respectively. If a - string: + the minimum and maximum value of the colormap, respectively. If a list + of 2-tuples, each image has an independent vmin/vmax, where each images + minimum/maximum values are specified by the 2-tuples in the list ordered + from first image to last. If a string: * `'auto0'`: all images have same vmin/vmax, which have the same absolute - value, and come from the minimum or maximum across all - images, whichever has the larger absolute value + value, and come from the minimum or maximum across all + images, whichever has the larger absolute value + * `'auto/auto1'`: all images have same vmin/vmax, which are the - minimum/maximum values across all images + minimum/maximum values across all images + * `'auto2'`: all images have same vmin/vmax, which are the mean (across - all images) minus/ plus 2 std dev (across all images) + all images) minus/ plus 2 std dev (across all images) + * `'auto3'`: all images have same vmin/vmax, chosen so as to map the - 10th/90th percentile values to the 10th/90th percentile of - the display intensity range. For example: vmin is the 10th - percentile image value minus 1/8 times the difference - between the 90th and 10th percentile + 10th/90th percentile values to the 10th/90th percentile of + the display intensity range. For example: vmin is the 10th + percentile image value minus 1/8 times the difference + between the 90th and 10th percentile + + * `'auto[X]row'`: each row of the figure has the same vmin/vmax, which are + computed using the auto[X] methods described above Eg. + `'auto1row'`, `'auto2row'`, or `'auto3row'`Ie. min/max, + mean minus/plus 2 std dev, or percentile statistics are + computed across all images in a given row, and those + values are used as the vmin/vmax for all images in that row. + High pass and low pass residuals have independent vmin/vmax + based on min/max of the residual image itself. + + * `'auto[X]col'`: each column of the figure has the same vmin/vmax, which are + computed using the auto[X] methods described above Eg. + `'auto1col'`, `'auto2col'`, or `'auto3col'`. + + * `'auto[X]colcomplex'`: vmin and vmax for each column is computed across both the real + and imaginary parts of all images in that column. + * `'indep0'`: each image has an independent vmin/vmax, which have the - same absolute value, which comes from either their - minimum or maximum value, whichever has the larger - absolute value. + same absolute value, which comes from either their + minimum or maximum value, whichever has the larger + absolute value. + * `'indep1'`: each image has an independent vmin/vmax, which are their - minimum/maximum values + minimum/maximum values. + * `'indep2'`: each image has an independent vmin/vmax, which is their - mean minus/plus 2 std dev + mean minus/plus 2 std dev. + * `'indep3'`: each image has an independent vmin/vmax, chosen so that - the 10th/90th percentile values map to the 10th/90th - percentile intensities. - NOTE: this only affects the behavior for grayscale images. RGB images - will always be displayed with vrange [0, 1] (for floats) or [0, 255] - (for ints), because of how matplotlib handles them. + the 10th/90th percentile values map to the 10th/90th + percentile intensities. + zoom : `float` ratio of display pixels to image pixels. if >1, must be an integer. If <1, must be 1/d where d is a a divisor of the size of the largest @@ -759,18 +840,17 @@ def imshow(image, vrange='indep1', zoom=1, title='', col_wrap=None, ax=None, # Process complex images for plotting, double-check image size to see if we # have RGB(A) images image, title, contains_rgb = _process_signal(image, title, plot_complex) - # make sure we can properly zoom all images zooms, max_shape = _check_zooms(image, zoom, any(contains_rgb)) # get the figure and axes created - fig, axes = _setup_figure(ax, col_wrap, image, zoom, max_shape, vert_pct) + fig, axes, n_cols = _setup_figure(ax, col_wrap, image, zoom, max_shape, vert_pct) if any(contains_rgb) and vrange != "indep1": warnings.warn("RGB images cannot have their vrange set: matplotlib " "will always show them with vrange [0, 1] (for floats) " "or [0, 255] (for ints).") - vrange_list, cmap = colormap_range(image, contains_rgb, vrange, cmap) + vrange_list, cmap = colormap_range(image, contains_rgb, vrange, cmap, n_cols) assert len(image) == len(vrange_list) @@ -806,27 +886,66 @@ def animshow(video, framerate=2., as_html5=True, repeat=False, Requires ipython to be installed. repeat : `bool` whether to loop the animation or just play it once - vrange : `tuple` or `str` - If a 2-tuple, specifies the image values vmin/vmax that are mapped to the minimum and - maximum value of the colormap, respectively. If a string: - - * `'auto/auto1'`: all images have same vmin/vmax, which are the minimum/maximum values - across all images - * `'auto2'`: all images have same vmin/vmax, which are the mean (across all images) minus/ - plus 2 std dev (across all images) - * `'auto3'`: all images have same vmin/vmax, chosen so as to map the 10th/90th percentile - values to the 10th/90th percentile of the display intensity range. For - example: vmin is the 10th percentile image value minus 1/8 times the - difference between the 90th and 10th percentile - * `'indep1'`: each image has an independent vmin/vmax, which are their minimum/maximum - values - * `'indep2'`: each image has an independent vmin/vmax, which is their mean minus/plus 2 - std dev - * `'indep3'`: each image has an independent vmin/vmax, chosen so that the 10th/90th - percentile values map to the 10th/90th percentile intensities. - NOTE: this only affects the behavior for grayscale images. RGB images - will always be displayed with vrange [0, 1] (for floats) or [0, 255] - (for ints), because of how matplotlib handles them. + vrange : `tuple` or `list` or `str` + + .. attention:: + this only affects the behavior for grayscale images. RGB images + will always be displayed with vrange [0, 1] (for floats) or [0, 255] + (for ints), because of how matplotlib handles them. + + If a 2-tuple, specifies the image values vmin/vmax that are mapped to + the minimum and maximum value of the colormap, respectively. If a list + of 2-tuples, each image has an independent vmin/vmax, where each images + minimum/maximum values are specified by the 2-tuples in the list ordered + from first image to last. If a string: + + * `'auto0'`: all images have same vmin/vmax, which have the same absolute + value, and come from the minimum or maximum across all + images, whichever has the larger absolute value + + * `'auto/auto1'`: all images have same vmin/vmax, which are the + minimum/maximum values across all images + + * `'auto2'`: all images have same vmin/vmax, which are the mean (across + all images) minus/ plus 2 std dev (across all images) + + * `'auto3'`: all images have same vmin/vmax, chosen so as to map the + 10th/90th percentile values to the 10th/90th percentile of + the display intensity range. For example: vmin is the 10th + percentile image value minus 1/8 times the difference + between the 90th and 10th percentile + + * `'auto[X]row'`: each row of the figure has the same vmin/vmax, which are + computed using the auto[X] methods described above Eg. + `'auto1row'`, `'auto2row'`, or `'auto3row'`Ie. min/max, + mean minus/plus 2 std dev, or percentile statistics are + computed across all images in a given row, and those + values are used as the vmin/vmax for all images in that row. + High pass and low pass residuals have independent vmin/vmax + based on min/max of the residual image itself. + + * `'auto[X]col'`: each column of the figure has the same vmin/vmax, which are + computed using the auto[X] methods described above Eg. + `'auto1col'`, `'auto2col'`, or `'auto3col'`. + + * `'auto[X]colcomplex'`: vmin and vmax for each column is computed across both the real + and imaginary parts of all images in that column. + + * `'indep0'`: each image has an independent vmin/vmax, which have the + same absolute value, which comes from either their + minimum or maximum value, whichever has the larger + absolute value. + + * `'indep1'`: each image has an independent vmin/vmax, which are their + minimum/maximum values. + + * `'indep2'`: each image has an independent vmin/vmax, which is their + mean minus/plus 2 std dev. + + * `'indep3'`: each image has an independent vmin/vmax, chosen so that + the 10th/90th percentile values map to the 10th/90th + percentile intensities. + zoom : `float` amount we zoom the video frames (must result in an integer when multiplied by video.shape[1:]) @@ -883,12 +1002,12 @@ def animshow(video, framerate=2., as_html5=True, repeat=False, title, vert_pct = _convert_title_to_list(title, video) video, title, contains_rgb = _process_signal(video, title, plot_complex, video=True) zooms, max_shape = _check_zooms(video, zoom, any(contains_rgb), video=True) - fig, axes = _setup_figure(ax, col_wrap, video, zoom, max_shape, vert_pct) + fig, axes, n_cols = _setup_figure(ax, col_wrap, video, zoom, max_shape, vert_pct) if any(contains_rgb) and vrange != "indep1": warnings.warn("RGB images cannot have their vrange set: matplotlib " "will always show them with vrange [0, 1] (for floats) " "or [0, 255] (for ints).") - vrange_list, cmap = colormap_range(video, contains_rgb, vrange, cmap) + vrange_list, cmap = colormap_range(video, contains_rgb, vrange, cmap, n_cols) assert len(video) == len(vrange_list) @@ -924,7 +1043,7 @@ def animate_video(t): return anim -def pyrshow(pyr_coeffs, is_complex=False, vrange='indep1', col_wrap=None, zoom=1, show_residuals=True, **kwargs): +def pyrshow(pyr_coeffs, is_complex=False, vrange='auto1row', col_wrap=None, zoom=1, show_residuals=True, **kwargs): """Display the coefficients of the pyramid in an orderly fashion Arguments @@ -934,24 +1053,66 @@ def pyrshow(pyr_coeffs, is_complex=False, vrange='indep1', col_wrap=None, zoom=1 is_complex : `bool` default False, indicates whether the pyramids is real or complex indicating whether the pyramid is complex or real - vrange : `tuple` or `str` - If a 2-tuple, specifies the image values vmin/vmax that are mapped to the minimum and - maximum value of the colormap, respectively. If a string: - - * `'auto/auto1'`: all images have same vmin/vmax, which are the minimum/maximum values - across all images - * `'auto2'`: all images have same vmin/vmax, which are the mean (across all images) minus/ - plus 2 std dev (across all images) - * `'auto3'`: all images have same vmin/vmax, chosen so as to map the 10th/90th percentile - values to the 10th/90th percentile of the display intensity range. For - example: vmin is the 10th percentile image value minus 1/8 times the - difference between the 90th and 10th percentile - * `'indep1'`: each image has an independent vmin/vmax, which are their minimum/maximum - values - * `'indep2'`: each image has an independent vmin/vmax, which is their mean minus/plus 2 - std dev - * `'indep3'`: each image has an independent vmin/vmax, chosen so that the 10th/90th - percentile values map to the 10th/90th percentile intensities. + vrange : `tuple` or `list` or `str` + + .. attention:: + this only affects the behavior for grayscale images. RGB images + will always be displayed with vrange [0, 1] (for floats) or [0, 255] + (for ints), because of how matplotlib handles them. + + If a 2-tuple, specifies the image values vmin/vmax that are mapped to + the minimum and maximum value of the colormap, respectively. If a list + of 2-tuples, each image has an independent vmin/vmax, where each images + minimum/maximum values are specified by the 2-tuples in the list ordered + from first image to last. If a string: + + * `'auto0'`: all images have same vmin/vmax, which have the same absolute + value, and come from the minimum or maximum across all + images, whichever has the larger absolute value + + * `'auto/auto1'`: all images have same vmin/vmax, which are the + minimum/maximum values across all images + + * `'auto2'`: all images have same vmin/vmax, which are the mean (across + all images) minus/ plus 2 std dev (across all images) + + * `'auto3'`: all images have same vmin/vmax, chosen so as to map the + 10th/90th percentile values to the 10th/90th percentile of + the display intensity range. For example: vmin is the 10th + percentile image value minus 1/8 times the difference + between the 90th and 10th percentile + + * `'auto[X]row'`: each row of the figure has the same vmin/vmax, which are + computed using the auto[X] methods described above Eg. + `'auto1row'`, `'auto2row'`, or `'auto3row'`Ie. min/max, + mean minus/plus 2 std dev, or percentile statistics are + computed across all images in a given row, and those + values are used as the vmin/vmax for all images in that row. + High pass and low pass residuals have independent vmin/vmax + based on min/max of the residual image itself. + + * `'auto[X]col'`: each column of the figure has the same vmin/vmax, which are + computed using the auto[X] methods described above Eg. + `'auto1col'`, `'auto2col'`, or `'auto3col'`. + + * `'auto[X]colcomplex'`: vmin and vmax for each column is computed across both the real + and imaginary parts of all images in that column. + + * `'indep0'`: each image has an independent vmin/vmax, which have the + same absolute value, which comes from either their + minimum or maximum value, whichever has the larger + absolute value. + + * `'indep1'`: each image has an independent vmin/vmax, which are their + minimum/maximum values. + + * `'indep2'`: each image has an independent vmin/vmax, which is their + mean minus/plus 2 std dev. + + * `'indep3'`: each image has an independent vmin/vmax, chosen so that + the 10th/90th percentile values map to the 10th/90th + percentile intensities. + col_wrap : `int` or None Only usable when the pyramid is one-dimensional (e.g., Gaussian or Laplacian Pyramid), otherwise the column wrap is determined by the number of bands. If not None, how many axes @@ -969,7 +1130,7 @@ def pyrshow(pyr_coeffs, is_complex=False, vrange='indep1', col_wrap=None, zoom=1 fig: `PyrFigure` the figure displaying the coefficients. """ - # right now, we do *not* do this the same as the old code. Instead of taking the coefficients + # right now, we do *not* do this the same as the old code. Instead of taking the coefficients # and arranging them in a spiral, we use imshow and arrange them neatly, displaying all at the # same size (and zoom / original image size clear), with different options for vrange. It # doesn't seem worth it to me to implement a version that looks like the old one, since that @@ -1040,4 +1201,4 @@ def pyrshow(pyr_coeffs, is_complex=False, vrange='indep1', col_wrap=None, zoom=1 "times, where this number is the height of the " f"pyramid{residual_err_msg}. " f"Instead, found:\n{err_msg}") - return imshow(imgs, vrange=vrange, col_wrap=col_wrap, zoom=zoom, title=titles, **kwargs) + return imshow(imgs, vrange=vrange, col_wrap=col_wrap, zoom=zoom, title=titles, **kwargs) \ No newline at end of file