diff --git a/docs/commandline.rst b/docs/commandline.rst index 18972d9..f18ed9e 100644 --- a/docs/commandline.rst +++ b/docs/commandline.rst @@ -1,7 +1,7 @@ Command Line Interface ====================== -The Pytools packages contain a command line executables for varous tasks. They can be invoke directly as an executable: +The Pytools packages contain a command line executables for various tasks. They can be invoke directly as an executable: .. code-block :: bash @@ -13,7 +13,7 @@ Or the preferred way using the `python` executable to execute the module entry p python -m mrc_visual_min_max --help -With either method of invoking the command line interface, the following sections descripts the sub-command available +With either method of invoking the command line interface, the following sections describes the sub-commands available and the command line options available. .. click:: pytools.ng.mrc2nifti:main diff --git a/docs/development.rst b/docs/development.rst index 9c290c9..37c2e99 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -68,7 +68,7 @@ line: .. code:: bash - python -m pytest tes + python -m pytest test Test Configuration diff --git a/pytools/ng/build_histogram.py b/pytools/ng/build_histogram.py index 38c6721..23db89c 100644 --- a/pytools/ng/build_histogram.py +++ b/pytools/ng/build_histogram.py @@ -169,17 +169,21 @@ def histogram_stats(hist, bin_edges): @click.option( "--output-json", type=click.Path(exists=False, dir_okay=False, resolve_path=True), - help='The output filename produced in JSON format with "min" and "max" data elements of a number ' - "value (double).", + help='The output filename produced in JSON format with "neuroglancerPrecomputedMin", ' + '"neuroglancerPrecomputedMax", "neuroglancerPrecomputedFloor" and "neuroglancerPrecomputedLimit" data ' + "elements of a double numeric value.", ) @click.version_option(__version__) def main(input_image, mad, sigma, percentile, output_json): """ - Reads the INPUT_IMAGE to compute a estimated minimum and maximum range to be used for visualization of the + Reads the INPUT_IMAGE to compute am estimated minimum and maximum range to be used for visualization of the data set. - The optional OUTPUT_JSON filename will be created and contain "min" and "max" data elements with a number value - (double). + The optional OUTPUT_JSON filename will be created with the following data elements with a double numeric value: + "neuroglancerPrecomputedMin" + "neuroglancerPrecomputedMax" + "neuroglancerPrecomputedFloor" + "neuroglancerPrecomputedLimit" """ logger = logging.getLogger() @@ -213,6 +217,7 @@ def main(input_image, mad, sigma, percentile, output_json): logger.info(f'Building histogram for "{reader.GetFileName()}"...') h, bins = stream_build_histogram(input_image, list(bin_edges)) + mids = 0.5 * (bins[1:] + bins[:-1]) logger.info("Computing statistics...") if mad: @@ -225,8 +230,6 @@ def main(input_image, mad, sigma, percentile, output_json): min_max = (stats["mean"] - stats["sigma"] * sigma, stats["mean"] + stats["sigma"] * sigma) elif percentile: - mids = 0.5 * (bins[1:] + bins[:-1]) - lower_quantile = (0.5 * (100 - percentile)) / 100.0 upper_quantile = percentile / 100.0 + lower_quantile logger.debug(f"quantiles: {lower_quantile} {upper_quantile}") @@ -241,8 +244,15 @@ def main(input_image, mad, sigma, percentile, output_json): else: raise RuntimeError("Missing expected argument") - output = {"min": float(min_max[0]), "max": float(min_max[1])} + floor_limit = weighted_quantile(mids, quantiles=[0.0, 1.0], sample_weight=h, values_sorted=True) + output = { + "neuroglancerPrecomputedMin": float(min_max[0]), + "neuroglancerPrecomputedMax": float(min_max[1]), + "neuroglancerPrecomputedFloor": float(floor_limit[0]), + "neuroglancerPrecomputedLimit": float(floor_limit[1]), + } + logger.debug(f"output: {output}") if output_json: import json diff --git a/test/fixtures.py b/test/fixtures.py index e63ff8b..4d83f82 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -13,18 +13,29 @@ # import pytest import SimpleITK as sitk +import numpy as np @pytest.fixture( scope="session", - params=[sitk.sitkUInt8, sitk.sitkInt16, sitk.sitkUInt16, sitk.sitkFloat32], + params=[sitk.sitkUInt8, sitk.sitkInt16, sitk.sitkUInt16, sitk.sitkFloat32, "uint16_uniform"], ) def image_mrc(request, tmp_path_factory): - pixel_type = request.param - print(f"Calling image_mrc with {sitk.GetPixelIDValueAsString(pixel_type)}") - fn = f"image_mrc_{sitk.GetPixelIDValueAsString(pixel_type).replace(' ', '_')}.mrc" - img = sitk.Image([10, 9, 8], pixel_type) - img.SetSpacing([1.1, 1.2, 1.3]) + if isinstance(request.param, str) and request.param == "uint16_uniform": + + print(f"Calling image_mrc with {request.param}") + fn = f"image_mrc_{request.param.replace(' ', '_')}.mrc" + + a = np.linspace(0, 2**16 - 1, num=2**16, dtype="uint16").reshape(16, 64, 64) + img = sitk.GetImageFromArray(a) + img.SetSpacing([1.23, 1.23, 4.96]) + else: + pixel_type = request.param + print(f"Calling image_mrc with {sitk.GetPixelIDValueAsString(pixel_type)}") + fn = f"image_mrc_{sitk.GetPixelIDValueAsString(pixel_type).replace(' ', '_')}.mrc" + img = sitk.Image([10, 9, 8], pixel_type) + img.SetSpacing([1.1, 1.2, 1.3]) + fn = tmp_path_factory.mktemp("data").joinpath(fn) sitk.WriteImage(img, str(fn)) return str(fn) diff --git a/test/test_histogram.py b/test/test_histogram.py index 315d6ef..b135bf2 100644 --- a/test/test_histogram.py +++ b/test/test_histogram.py @@ -108,22 +108,31 @@ def test_histogram_mai_help(cli_args): @pytest.mark.parametrize( - "image_mrc,expected_min, expected_max", - [(sitk.sitkUInt8, 0, 0), (sitk.sitkInt16, 0, 0), (sitk.sitkUInt16, 0, 0)], + "image_mrc,expected_min, expected_max, expected_floor, expected_limit", + [ + (sitk.sitkUInt8, 0, 0, 0, 0), + (sitk.sitkInt16, 0, 0, 0, 0), + (sitk.sitkUInt16, 0, 0, 0, 0), + ("uint16_uniform", 8191.5, 57343.5, 0, 65535), + ], indirect=["image_mrc"], ) -def test_build_histogram_main(image_mrc, expected_min, expected_max): +def test_build_histogram_main(image_mrc, expected_min, expected_max, expected_floor, expected_limit): runner = CliRunner() output_filename = "out.json" with runner.isolated_filesystem(): result = runner.invoke( - pytools.ng.build_histogram.main, [image_mrc, "--mad", "5", "--output-json", output_filename] + pytools.ng.build_histogram.main, [image_mrc, "--mad", "1.5", "--output-json", output_filename] ) assert not result.exception with open(output_filename) as fp: res = json.load(fp) - assert "min" in res - assert "max" in res - assert res["min"] == expected_min - assert res["max"] == expected_max + assert "neuroglancerPrecomputedMin" in res + assert "neuroglancerPrecomputedMax" in res + assert "neuroglancerPrecomputedFloor" in res + assert "neuroglancerPrecomputedLimit" in res + assert res["neuroglancerPrecomputedMin"] == expected_min + assert res["neuroglancerPrecomputedMax"] == expected_max + assert res["neuroglancerPrecomputedFloor"] == expected_floor + assert res["neuroglancerPrecomputedLimit"] == expected_limit diff --git a/test/test_mrc2ngpc.py b/test/test_mrc2ngpc.py index 338e447..a088412 100644 --- a/test/test_mrc2ngpc.py +++ b/test/test_mrc2ngpc.py @@ -47,5 +47,7 @@ def test_mrc2ngpc(image_mrc, expected_pixel_type): with open("mrc2ngpc-output.json") as fp: mm = json.load(fp) - assert "min" in mm - assert "max" in mm + assert "neuroglancerPrecomputedMin" in mm + assert "neuroglancerPrecomputedMin" in mm + assert "neuroglancerPrecomputedFloor" in mm + assert "neuroglancerPrecomputedLimit" in mm