From 28821834a2ff8b62d83594a78c1868fc19a0e6ac Mon Sep 17 00:00:00 2001 From: Bradley Lowekamp Date: Fri, 27 Oct 2023 10:35:38 -0400 Subject: [PATCH 1/9] Add middle_quantile parameter to ng parameter computation --- pytools/HedwigZarrImage.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/pytools/HedwigZarrImage.py b/pytools/HedwigZarrImage.py index 19efec7..ba47eaa 100644 --- a/pytools/HedwigZarrImage.py +++ b/pytools/HedwigZarrImage.py @@ -15,7 +15,7 @@ import SimpleITK as sitk import zarr -from typing import Tuple, Dict, List +from typing import Tuple, Dict, List, Optional from pytools.utils import OMEInfo import logging import math @@ -203,11 +203,21 @@ def shader_type( return "Grayscale" return "MultiChannel" - def neuroglancer_shader_parameters(self, mad_scale=3) -> dict: + def neuroglancer_shader_parameters( + self, *, mad_scale=3, middle_quantile: Optional[Tuple[float, float]] = None + ) -> dict: """ Produces the "shaderParameters" portion of the metadata for Neuroglancer + :param mad_scale: + :param middle_quantile: returns JSON serializable object """ + + if middle_quantile: + assert len(middle_quantile) == 2 + assert 0.0 <= middle_quantile[0] <= 1.0 + assert 0.0 <= middle_quantile[1] <= 1.0 + if self.ome_info is None: return {} @@ -248,8 +258,12 @@ def neuroglancer_shader_parameters(self, mad_scale=3) -> dict: # replace non-alpha numeric with a underscore name = re.sub(r"[^a-zA-Z0-9]+", "_", c_name.lower()) - stats = self._image_statistics(channel=c) - range = (stats["median"] - stats["mad"] * mad_scale, stats["median"] + stats["mad"] * mad_scale) + stats = self._image_statistics(quantiles=middle_quantile, channel=c) + if middle_quantile: + range = (stats["quantiles"][middle_quantile[0]], stats["quantiles"][middle_quantile[1]]) + else: + range = (stats["median"] - stats["mad"] * mad_scale, stats["median"] + stats["mad"] * mad_scale) + range = (max(range[0], stats["min"]), min(range[1], stats["max"])) json_channel_array.append( @@ -314,11 +328,12 @@ def _chunk_logic_dim(drequest: int, dshape: int) -> int: return drequest return dshape - def _image_statistics(self, channel=None) -> Dict[str, List[int]]: + def _image_statistics(self, quantiles=None, channel=None) -> Dict[str, List[int]]: """Processes the full resolution Zarr image. Dask is used for parallel reading and statistics computation. The global scheduler is used for all operations which can be changed with standard Dask configurations. :param channel: The index of the channel to perform calculation on + :param quantiles: values of quantiles to return in option "quantiles" element. :returns: The resulting dictionary will contain the following data elements: "min", @@ -355,6 +370,9 @@ def _image_statistics(self, channel=None) -> Dict[str, List[int]]: stats = histogram_robust_stats(h, bins) stats.update(histogram_stats(h, bins)) stats["min"], stats["max"] = weighted_quantile(mids, quantiles=[0.0, 1.0], sample_weight=h, values_sorted=True) + if quantiles: + quantile_value = weighted_quantile(mids, quantiles=quantiles, sample_weight=h, values_sorted=True) + stats["quantiles"] = {q: v for q, v in zip(quantiles, quantile_value)} logger.debug(f"stats: {stats}") return stats From 043b66769d8e38a688f41e881d8a970ffb006bef Mon Sep 17 00:00:00 2001 From: Bradley Lowekamp Date: Fri, 27 Oct 2023 13:49:39 -0400 Subject: [PATCH 2/9] use 0.9999 quantil for upper window for multichannel images --- pytools/HedwigZarrImage.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pytools/HedwigZarrImage.py b/pytools/HedwigZarrImage.py index ba47eaa..769950a 100644 --- a/pytools/HedwigZarrImage.py +++ b/pytools/HedwigZarrImage.py @@ -258,7 +258,13 @@ def neuroglancer_shader_parameters( # replace non-alpha numeric with a underscore name = re.sub(r"[^a-zA-Z0-9]+", "_", c_name.lower()) - stats = self._image_statistics(quantiles=middle_quantile, channel=c) + upper_quantile = 0.9999 + stats = self._image_statistics( + quantiles=[ + upper_quantile, + ].a(middle_quantile), + channel=c, + ) if middle_quantile: range = (stats["quantiles"][middle_quantile[0]], stats["quantiles"][middle_quantile[1]]) else: @@ -269,7 +275,7 @@ def neuroglancer_shader_parameters( json_channel_array.append( { "range": [math.floor(range[0]), math.ceil(range[1])], - "window": [math.floor(stats["min"]), math.ceil(stats["max"])], + "window": [math.floor(stats["min"]), math.ceil(stats["quantiles"][upper_quantile])], "name": name, "color": color_sequence[c], "channel": c, From 6b9adf1c49d705ac4db8d8ebe4ba3310fb1f42aa Mon Sep 17 00:00:00 2001 From: Bradley Lowekamp Date: Fri, 27 Oct 2023 13:51:12 -0400 Subject: [PATCH 3/9] ignore 0 values for quantiles computation --- pytools/HedwigZarrImage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pytools/HedwigZarrImage.py b/pytools/HedwigZarrImage.py index 769950a..b25779e 100644 --- a/pytools/HedwigZarrImage.py +++ b/pytools/HedwigZarrImage.py @@ -377,6 +377,7 @@ def _image_statistics(self, quantiles=None, channel=None) -> Dict[str, List[int] stats.update(histogram_stats(h, bins)) stats["min"], stats["max"] = weighted_quantile(mids, quantiles=[0.0, 1.0], sample_weight=h, values_sorted=True) if quantiles: + h[0] = 0 quantile_value = weighted_quantile(mids, quantiles=quantiles, sample_weight=h, values_sorted=True) stats["quantiles"] = {q: v for q, v in zip(quantiles, quantile_value)} logger.debug(f"stats: {stats}") From b71a94ad88d12768db497aea706c91fb6880fd02 Mon Sep 17 00:00:00 2001 From: Bradley Lowekamp Date: Mon, 13 Nov 2023 14:30:41 -0500 Subject: [PATCH 4/9] Refactor new multi-channel parameter code into funciton. --- pytools/HedwigZarrImage.py | 108 +++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 51 deletions(-) diff --git a/pytools/HedwigZarrImage.py b/pytools/HedwigZarrImage.py index b25779e..58461c9 100644 --- a/pytools/HedwigZarrImage.py +++ b/pytools/HedwigZarrImage.py @@ -203,6 +203,59 @@ def shader_type( return "Grayscale" return "MultiChannel" + def _neuroglancer_shader_parameters_multichannel( + self, *, mad_scale=3, middle_quantile: Optional[Tuple[float, float]] = None + ) -> dict: + assert self._ome_ngff_multiscale_dims()[1] == "C" + + if len(list(self.ome_info.channel_names(self.ome_idx))) != self.shape[1]: + raise RuntimeError( + f"Mismatch of number of Channels! Array has {self.shape[1]} but there" + f"are {len(list(self.ome_info.channel_names(self.ome_idx)))} names:" + f"{list(self.ome_info.channel_names(self.ome_idx))}" + ) + + color_sequence = ["red", "green", "blue", "cyan", "yellow", "magenta"] + + if self.shape[1] > len(color_sequence): + raise RuntimeError( + f"Too many channels! The array has {self.shape[1]} channels but" + f" only {len(color_sequence)} is supported!" + ) + + json_channel_array = [] + + for c, c_name in enumerate(self.ome_info.channel_names(self.ome_idx)): + logger.debug(f"Processing channel: {c_name}") + + # replace non-alpha numeric with an underscore + name = re.sub(r"[^a-zA-Z0-9]+", "_", c_name.lower()) + + upper_quantile = 0.9999 + stats = self._image_statistics( + quantiles=[*middle_quantile, upper_quantile] if middle_quantile else [upper_quantile], channel=c + ) + if middle_quantile: + range = (stats["quantiles"][middle_quantile[0]], stats["quantiles"][middle_quantile[1]]) + else: + range = (stats["median"] - stats["mad"] * mad_scale, stats["median"] + stats["mad"] * mad_scale) + + range = (max(range[0], stats["min"]), min(range[1], stats["max"])) + + json_channel_array.append( + { + "range": [math.floor(range[0]), math.ceil(range[1])], + "window": [math.floor(stats["min"]), math.ceil(stats["quantiles"][upper_quantile])], + "name": name, + "color": color_sequence[c], + "channel": c, + "clamp": False, + "enabled": True, + } + ) + + return {"brightness": 0.0, "contrast": 0.0, "channelArray": json_channel_array} + def neuroglancer_shader_parameters( self, *, mad_scale=3, middle_quantile: Optional[Tuple[float, float]] = None ) -> dict: @@ -217,6 +270,7 @@ def neuroglancer_shader_parameters( assert len(middle_quantile) == 2 assert 0.0 <= middle_quantile[0] <= 1.0 assert 0.0 <= middle_quantile[1] <= 1.0 + assert middle_quantile[0] < middle_quantile[1] if self.ome_info is None: return {} @@ -234,57 +288,9 @@ def neuroglancer_shader_parameters( } if _shader_type == "MultiChannel": - assert self._ome_ngff_multiscale_dims()[1] == "C" - - if len(list(self.ome_info.channel_names(self.ome_idx))) != self.shape[1]: - raise RuntimeError( - f"Mismatch of number of Channels! Array has {self.shape[1]} but there" - f"are {len(list(self.ome_info.channel_names(self.ome_idx)))} names:" - f"{list(self.ome_info.channel_names(self.ome_idx))}" - ) - color_sequence = ["red", "green", "blue", "cyan", "yellow", "magenta"] - - if self.shape[1] > len(color_sequence): - raise RuntimeError( - f"Too many channels! The array has {self.shape[1]} channels but" - f" only {len(color_sequence)} is supported!" - ) - - json_channel_array = [] - - for c, c_name in enumerate(self.ome_info.channel_names(self.ome_idx)): - logger.debug(f"Processing channel: {c_name}") - - # replace non-alpha numeric with a underscore - name = re.sub(r"[^a-zA-Z0-9]+", "_", c_name.lower()) - - upper_quantile = 0.9999 - stats = self._image_statistics( - quantiles=[ - upper_quantile, - ].a(middle_quantile), - channel=c, - ) - if middle_quantile: - range = (stats["quantiles"][middle_quantile[0]], stats["quantiles"][middle_quantile[1]]) - else: - range = (stats["median"] - stats["mad"] * mad_scale, stats["median"] + stats["mad"] * mad_scale) - - range = (max(range[0], stats["min"]), min(range[1], stats["max"])) - - json_channel_array.append( - { - "range": [math.floor(range[0]), math.ceil(range[1])], - "window": [math.floor(stats["min"]), math.ceil(stats["quantiles"][upper_quantile])], - "name": name, - "color": color_sequence[c], - "channel": c, - "clamp": False, - "enabled": True, - } - ) - - return {"brightness": 0.0, "contrast": 0.0, "channelArray": json_channel_array} + return self._neuroglancer_shader_parameters_multichannel( + mad_scale=mad_scale, middle_quantile=middle_quantile + ) raise RuntimeError(f'Unknown shader type: "{self.shader_type}"') From 2fa6203dd3275dfacd9de54dd6215809ca6f4dfd Mon Sep 17 00:00:00 2001 From: Bradley Lowekamp Date: Mon, 13 Nov 2023 14:41:49 -0500 Subject: [PATCH 5/9] improve min max code style --- pytools/HedwigZarrImage.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pytools/HedwigZarrImage.py b/pytools/HedwigZarrImage.py index 58461c9..bbd59c7 100644 --- a/pytools/HedwigZarrImage.py +++ b/pytools/HedwigZarrImage.py @@ -179,11 +179,11 @@ def extract_2d( if is_vector: img.ToScalarImage(True) - min_max = sitk.MinimumMaximum(img) + min, max = sitk.MinimumMaximum(img) - logger.debug(f"Adjusting output pixel intensity range from {min_max} -> {(0, 255)}.") + logger.debug(f"Adjusting output pixel intensity range from {min, max} -> {(0, 255)}.") - img = sitk.ShiftScale(img, -min_max[0], 255.0 / (min_max[1] - min_max[0]), sitk.sitkUInt8) + img = sitk.ShiftScale(img, -min, 255.0 / (max - min), sitk.sitkUInt8) if is_vector: img.ToVectorImage(True) From 901995587b057fad4221afbc8777a994c7f84e60 Mon Sep 17 00:00:00 2001 From: Bradley Lowekamp Date: Mon, 13 Nov 2023 14:42:32 -0500 Subject: [PATCH 6/9] Add zero_black_quantiles parameter --- pytools/HedwigZarrImage.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pytools/HedwigZarrImage.py b/pytools/HedwigZarrImage.py index bbd59c7..7720c19 100644 --- a/pytools/HedwigZarrImage.py +++ b/pytools/HedwigZarrImage.py @@ -233,7 +233,9 @@ def _neuroglancer_shader_parameters_multichannel( upper_quantile = 0.9999 stats = self._image_statistics( - quantiles=[*middle_quantile, upper_quantile] if middle_quantile else [upper_quantile], channel=c + quantiles=[*middle_quantile, upper_quantile] if middle_quantile else [upper_quantile], + channel=c, + zero_black_quantiles=True, ) if middle_quantile: range = (stats["quantiles"][middle_quantile[0]], stats["quantiles"][middle_quantile[1]]) @@ -340,7 +342,7 @@ def _chunk_logic_dim(drequest: int, dshape: int) -> int: return drequest return dshape - def _image_statistics(self, quantiles=None, channel=None) -> Dict[str, List[int]]: + def _image_statistics(self, quantiles=None, channel=None, *, zero_black_quantiles=False) -> Dict[str, List[int]]: """Processes the full resolution Zarr image. Dask is used for parallel reading and statistics computation. The global scheduler is used for all operations which can be changed with standard Dask configurations. @@ -383,7 +385,9 @@ def _image_statistics(self, quantiles=None, channel=None) -> Dict[str, List[int] stats.update(histogram_stats(h, bins)) stats["min"], stats["max"] = weighted_quantile(mids, quantiles=[0.0, 1.0], sample_weight=h, values_sorted=True) if quantiles: - h[0] = 0 + if zero_black_quantiles: + h[0] = 0 + quantile_value = weighted_quantile(mids, quantiles=quantiles, sample_weight=h, values_sorted=True) stats["quantiles"] = {q: v for q, v in zip(quantiles, quantile_value)} logger.debug(f"stats: {stats}") From a64acb237a504f2cc17ccc8e69b813c6c26c2c5a Mon Sep 17 00:00:00 2001 From: Bradley Lowekamp Date: Mon, 13 Nov 2023 15:09:19 -0500 Subject: [PATCH 7/9] Add testing for computing multiple quantiles values --- test/test_histogram.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/test_histogram.py b/test/test_histogram.py index 137b063..54a450e 100644 --- a/test/test_histogram.py +++ b/test/test_histogram.py @@ -38,6 +38,14 @@ def test_weighted_quantile(): assert pytools_hist.weighted_quantile(v, [0.5], sample_weight=h) == approx(4) assert pytools_hist.weighted_quantile(v, [1], sample_weight=h) == approx(6) + data = list(range(100)) + h, b = np.histogram(data, bins=np.arange(-0.5, 100.5)) + v = 0.5 * (b[1:] + b[:-1]) + print(b) + quantiles_values = list(pytools_hist.weighted_quantile(v, [0.25, 0.5, 0.75], sample_weight=h)) + for value, baseline in zip(quantiles_values, [24.5, 49.5, 74.5]): + assert value == approx(baseline) + def test_histogram_stats(): data = [3] From 38067062103f6e5368aa9e23ce2aca0a21e1a840 Mon Sep 17 00:00:00 2001 From: Bradley Lowekamp Date: Mon, 13 Nov 2023 16:10:09 -0500 Subject: [PATCH 8/9] Add testing for quantile parameters for multichannel params --- pytools/HedwigZarrImage.py | 4 ++-- test/test_HedwigZarrImage.py | 41 +++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/pytools/HedwigZarrImage.py b/pytools/HedwigZarrImage.py index 7720c19..71d204e 100644 --- a/pytools/HedwigZarrImage.py +++ b/pytools/HedwigZarrImage.py @@ -204,7 +204,7 @@ def shader_type( return "MultiChannel" def _neuroglancer_shader_parameters_multichannel( - self, *, mad_scale=3, middle_quantile: Optional[Tuple[float, float]] = None + self, *, mad_scale=3, middle_quantile: Optional[Tuple[float, float]] = None, zero_black_quantiles=True ) -> dict: assert self._ome_ngff_multiscale_dims()[1] == "C" @@ -235,7 +235,7 @@ def _neuroglancer_shader_parameters_multichannel( stats = self._image_statistics( quantiles=[*middle_quantile, upper_quantile] if middle_quantile else [upper_quantile], channel=c, - zero_black_quantiles=True, + zero_black_quantiles=zero_black_quantiles, ) if middle_quantile: range = (stats["quantiles"][middle_quantile[0]], stats["quantiles"][middle_quantile[1]]) diff --git a/test/test_HedwigZarrImage.py b/test/test_HedwigZarrImage.py index 9a978ad..fcf5806 100644 --- a/test/test_HedwigZarrImage.py +++ b/test/test_HedwigZarrImage.py @@ -11,8 +11,9 @@ # See the License for the specific language governing permissions and # limitations under the License. # - +import numpy as np import pytest +from pytest import approx import shutil from pytools.HedwigZarrImages import HedwigZarrImages from pytools.HedwigZarrImage import HedwigZarrImage @@ -79,6 +80,44 @@ def test_HedwigZarrImage_info_for_czi(data_path, zarr_name, attrs): shader_params[param_key] == len(z_img.neuroglancer_shader_parameters()[param_key]) +@pytest.mark.parametrize( + "zarr_name, key", + [ + ("KC_M3_S2_ReducedImageSubset2.zarr", "Scene #0"), + ], +) +def test_HedwigZarrImage_info_for_czi_quantiles(data_path, zarr_name, key): + """ + Test that the quantiles are computed correctly for the neuroglancer shader parameters + """ + zi = HedwigZarrImages(data_path / zarr_name) + assert zi.ome_xml_path is not None + image_names = list(zi.get_series_keys()) + assert len(image_names) == 3 + assert all(image_names) + + z_img = zi[key] + assert z_img._ome_ngff_multiscale_dims() == "TCZYX" + + shader_params = z_img._neuroglancer_shader_parameters_multichannel(zero_black_quantiles=False) + + for idx, channel_params in enumerate(shader_params["channelArray"]): + param_window_min, param_window_max = channel_params["window"] + qvalues = np.quantile(z_img._ome_ngff_multiscale_get_array(0)[:, idx, ...], [0.0, 0.9999]) + + assert param_window_min == approx(qvalues[0], abs=1) + # not sure why this is not more accurate + assert param_window_max == approx(qvalues[1], abs=20) + + shader_params = z_img.neuroglancer_shader_parameters(middle_quantile=(0.25, 0.75)) + + for idx, channel_params in enumerate(shader_params["channelArray"]): + param_range_min, param_range_max = channel_params["range"] + qvalues = np.quantile(z_img._ome_ngff_multiscale_get_array(0)[:, idx, ...], [0.25, 0.75]) + assert param_range_min == approx(qvalues[0], abs=1) + assert param_range_max == approx(qvalues[1], abs=1) + + @pytest.mark.parametrize("targetx, targety", [(300, 300), (600, 600), (1024, 1024)]) def test_hedwigimage_extract_2d(data_path, targetx, targety): """ From 935afa506db6eed9470d1025f7f872cd16e83046 Mon Sep 17 00:00:00 2001 From: Bradley Lowekamp Date: Tue, 14 Nov 2023 09:29:21 -0500 Subject: [PATCH 9/9] Update documentation to neuroglancer_shader_parameters methods --- pytools/HedwigZarrImage.py | 62 ++++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/pytools/HedwigZarrImage.py b/pytools/HedwigZarrImage.py index 71d204e..44ac0ab 100644 --- a/pytools/HedwigZarrImage.py +++ b/pytools/HedwigZarrImage.py @@ -204,8 +204,37 @@ def shader_type( return "MultiChannel" def _neuroglancer_shader_parameters_multichannel( - self, *, mad_scale=3, middle_quantile: Optional[Tuple[float, float]] = None, zero_black_quantiles=True + self, + *, + mad_scale=3, + middle_quantile: Optional[Tuple[float, float]] = None, + zero_black_quantiles=True, + upper_quantile=0.9999, ) -> dict: + """ + Produces the "shaderParameters" portion of the metadata for Neuroglancer when the shader type is MultiChannel. + + The output window parameters are used for the visible histogram range. The computation improves the robustness + to outliers and the background pixel values with the zero_black_quantiles option and the upper_quantile value. + + :param mad_scale: The scale factor for the robust median absolute deviation (MAD) about the median to produce + the minimum and maximum range that is used to select the visible pixel intensities. + :param middle_quantile: If not None then the range is computed from the provided quantiles of the image data. + The middle_quantile is a tuple of two floats that are between 0.0 and 1.0. The first value is the lower + quantile and the second value is the upper quantile. + :param zero_black_quantiles: If True then the zero values are removed from the histogram before computing the + quantiles. + :param upper_quantile: The upper quantile to use for the "window", which is the extent of the visible histogram. + :return: The dictionary of the shader parameters suitable for JSON serialization. + + """ + + if middle_quantile: + assert len(middle_quantile) == 2 + assert 0.0 <= middle_quantile[0] <= 1.0 + assert 0.0 <= middle_quantile[1] <= 1.0 + assert middle_quantile[0] < middle_quantile[1] + assert self._ome_ngff_multiscale_dims()[1] == "C" if len(list(self.ome_info.channel_names(self.ome_idx))) != self.shape[1]: @@ -231,7 +260,6 @@ def _neuroglancer_shader_parameters_multichannel( # replace non-alpha numeric with an underscore name = re.sub(r"[^a-zA-Z0-9]+", "_", c_name.lower()) - upper_quantile = 0.9999 stats = self._image_statistics( quantiles=[*middle_quantile, upper_quantile] if middle_quantile else [upper_quantile], channel=c, @@ -262,18 +290,25 @@ def neuroglancer_shader_parameters( self, *, mad_scale=3, middle_quantile: Optional[Tuple[float, float]] = None ) -> dict: """ - Produces the "shaderParameters" portion of the metadata for Neuroglancer - :param mad_scale: - :param middle_quantile: - returns JSON serializable object + Produces the "shaderParameters" portion of the metadata for Neuroglancer. + + Determines which shader type to use to render the image. The shader type is one of: RGB, Grayscale, or + MultiChannel. The shader parameters are computed from the full resolution Zarr image. Dask is used for parallel + reading and statistics computation. The global scheduler is used for all operations which can be changed with + standard Dask configurations. + + For the MultiChannel shader type the default algorithm for the range is to compute the robust median absolute + deviation (MAD) about the median to produce the minimum and maximum range. If the middle_quantile is not None + then the range is computed from the provided quantiles of the image data. + + :param mad_scale: The scale factor for the robust median absolute deviation (MAD) about the median to produce + the minimum and maximum range that is used to select the visible pixel intensities. + :param middle_quantile: If not None then the range is computed from the provided quantiles of the image data. + The middle_quantile is a tuple of two floats that are between 0.0 and 1.0. The first value is the lower + quantile and the second value is the upper quantile. The range is computed from the lower and upper quantiles. + :return: The dictionary of the shader parameters suitable for JSON serialization """ - if middle_quantile: - assert len(middle_quantile) == 2 - assert 0.0 <= middle_quantile[0] <= 1.0 - assert 0.0 <= middle_quantile[1] <= 1.0 - assert middle_quantile[0] < middle_quantile[1] - if self.ome_info is None: return {} @@ -356,7 +391,8 @@ def _image_statistics(self, quantiles=None, channel=None, *, zero_black_quantile "mad", "mean", "var", - "sigma" + "sigma", + "quantiles" (if quantiles is not None) """