From 3feffbd08710c9ee3a3beafc204313feb35c7918 Mon Sep 17 00:00:00 2001 From: Bradley Lowekamp Date: Fri, 19 Aug 2022 12:12:42 -0400 Subject: [PATCH 1/2] Change JSON output field values to string of integer Cast the floating point value to a rounded integer converted to a string. --- pytools/ng/build_histogram.py | 18 ++++++++++-------- test/test_histogram.py | 14 +++++++++----- test/test_mrc2ngpc.py | 4 ++++ 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/pytools/ng/build_histogram.py b/pytools/ng/build_histogram.py index 23db89c..063fb74 100644 --- a/pytools/ng/build_histogram.py +++ b/pytools/ng/build_histogram.py @@ -18,6 +18,7 @@ import SimpleITK as sitk from pytools.utils import MutuallyExclusiveOption from pytools import __version__ +from math import floor, ceil def weighted_quantile(values, quantiles, sample_weight=None, values_sorted=False, old_style=False): @@ -66,7 +67,7 @@ def stream_build_histogram(filename: str, histogram_bin_edges, extract_axis=1, d Read image slice by slice, and build a histogram. The image file must be readable by SimpleITK. The SimpleITK is expected to support streaming the file format. - The np.histogram function is run on each image slice with the provided hitogram_bin_edges, and + The np.histogram function is run on each image slice with the provided histogram_bin_edges, and accumulated for the results. :param filename: The path to the image file to read. MRC file type is recommend. @@ -176,10 +177,10 @@ def histogram_stats(hist, bin_edges): @click.version_option(__version__) def main(input_image, mad, sigma, percentile, output_json): """ - Reads the INPUT_IMAGE to compute am estimated minimum and maximum range to be used for visualization of the - data set. + Reads the INPUT_IMAGE to compute an estimated minimum and maximum range to be used for visualization of the + data set. The image is required to have an integer pixel type. - The optional OUTPUT_JSON filename will be created with the following data elements with a double numeric value: + The optional OUTPUT_JSON filename will be created with the following data elements with integer values as strings: "neuroglancerPrecomputedMin" "neuroglancerPrecomputedMax" "neuroglancerPrecomputedFloor" @@ -247,11 +248,12 @@ def main(input_image, mad, sigma, percentile, output_json): 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]), + "neuroglancerPrecomputedMin": str(floor(min_max[0])), + "neuroglancerPrecomputedMax": str(ceil(min_max[1])), + "neuroglancerPrecomputedFloor": str(floor(floor_limit[0])), + "neuroglancerPrecomputedLimit": str(ceil(floor_limit[1])), } + logger.debug(f"output: {output}") if output_json: import json diff --git a/test/test_histogram.py b/test/test_histogram.py index b135bf2..574ac47 100644 --- a/test/test_histogram.py +++ b/test/test_histogram.py @@ -113,7 +113,7 @@ def test_histogram_mai_help(cli_args): (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), + ("uint16_uniform", 8191, 57344, 0, 65535), ], indirect=["image_mrc"], ) @@ -132,7 +132,11 @@ def test_build_histogram_main(image_mrc, expected_min, expected_max, expected_fl 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 + assert float(res["neuroglancerPrecomputedMin"]) == expected_min + assert float(res["neuroglancerPrecomputedMax"]) == expected_max + assert float(res["neuroglancerPrecomputedFloor"]) == expected_floor + assert float(res["neuroglancerPrecomputedLimit"]) == expected_limit + assert type(res["neuroglancerPrecomputedMin"]) == str + assert type(res["neuroglancerPrecomputedMax"]) == str + assert type(res["neuroglancerPrecomputedFloor"]) == str + assert type(res["neuroglancerPrecomputedLimit"]) == str diff --git a/test/test_mrc2ngpc.py b/test/test_mrc2ngpc.py index a088412..10a1ec0 100644 --- a/test/test_mrc2ngpc.py +++ b/test/test_mrc2ngpc.py @@ -51,3 +51,7 @@ def test_mrc2ngpc(image_mrc, expected_pixel_type): assert "neuroglancerPrecomputedMin" in mm assert "neuroglancerPrecomputedFloor" in mm assert "neuroglancerPrecomputedLimit" in mm + assert type(mm["neuroglancerPrecomputedMin"]) == str + assert type(mm["neuroglancerPrecomputedMax"]) == str + assert type(mm["neuroglancerPrecomputedFloor"]) == str + assert type(mm["neuroglancerPrecomputedLimit"]) == str From 3f60b074c41bfa3b2670874f441f1a925b7b21ef Mon Sep 17 00:00:00 2001 From: Bradley Lowekamp Date: Fri, 19 Aug 2022 14:56:20 -0400 Subject: [PATCH 2/2] Add clamp option Optionally ensure that min and max are withon floor and limit. --- pytools/ng/build_histogram.py | 10 +++++++++- test/fixtures.py | 12 +++++++++++- test/test_histogram.py | 23 ++++++++++++++--------- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/pytools/ng/build_histogram.py b/pytools/ng/build_histogram.py index 063fb74..486040c 100644 --- a/pytools/ng/build_histogram.py +++ b/pytools/ng/build_histogram.py @@ -167,6 +167,11 @@ def histogram_stats(hist, bin_edges): mutually_exclusive=["sigma", "mad"], help="Use INPUT_IMAGE's middle percentile (option's value) of data for minimum and maximum range.", ) +@click.option( + "--clamp/--no-clamp", + default=False, + help="Clamps minimum and maximum range to existing intensity values (floor and limit).", +) @click.option( "--output-json", type=click.Path(exists=False, dir_okay=False, resolve_path=True), @@ -175,7 +180,7 @@ def histogram_stats(hist, bin_edges): "elements of a double numeric value.", ) @click.version_option(__version__) -def main(input_image, mad, sigma, percentile, output_json): +def main(input_image, mad, sigma, percentile, clamp, output_json): """ Reads the INPUT_IMAGE to compute an estimated minimum and maximum range to be used for visualization of the data set. The image is required to have an integer pixel type. @@ -247,6 +252,9 @@ def main(input_image, mad, sigma, percentile, output_json): floor_limit = weighted_quantile(mids, quantiles=[0.0, 1.0], sample_weight=h, values_sorted=True) + if clamp: + min_max = (max(min_max[0], floor_limit[0]), min(min_max[1], floor_limit[1])) + output = { "neuroglancerPrecomputedMin": str(floor(min_max[0])), "neuroglancerPrecomputedMax": str(ceil(min_max[1])), diff --git a/test/fixtures.py b/test/fixtures.py index 4d83f82..97605bf 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -18,7 +18,7 @@ @pytest.fixture( scope="session", - params=[sitk.sitkUInt8, sitk.sitkInt16, sitk.sitkUInt16, sitk.sitkFloat32, "uint16_uniform"], + params=[sitk.sitkUInt8, sitk.sitkInt16, sitk.sitkUInt16, sitk.sitkFloat32, "uint16_uniform", "uint8_bimodal"], ) def image_mrc(request, tmp_path_factory): if isinstance(request.param, str) and request.param == "uint16_uniform": @@ -29,6 +29,16 @@ def image_mrc(request, tmp_path_factory): 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]) + + elif isinstance(request.param, str) and request.param == "uint8_bimodal": + + print(f"Calling image_mrc with {request.param}") + fn = f"image_mrc_{request.param.replace(' ', '_')}.mrc" + + a = np.zeros([16, 16, 16], np.uint8) + a[len(a) // 2 :] = 255 + img = sitk.GetImageFromArray(a) + img.SetSpacing([12.3, 12.3, 56.7]) else: pixel_type = request.param print(f"Calling image_mrc with {sitk.GetPixelIDValueAsString(pixel_type)}") diff --git a/test/test_histogram.py b/test/test_histogram.py index 574ac47..aea8c6b 100644 --- a/test/test_histogram.py +++ b/test/test_histogram.py @@ -108,22 +108,27 @@ def test_histogram_mai_help(cli_args): @pytest.mark.parametrize( - "image_mrc,expected_min, expected_max, expected_floor, expected_limit", + "image_mrc,expected_min, expected_max, expected_floor, expected_limit, clamp", [ - (sitk.sitkUInt8, 0, 0, 0, 0), - (sitk.sitkInt16, 0, 0, 0, 0), - (sitk.sitkUInt16, 0, 0, 0, 0), - ("uint16_uniform", 8191, 57344, 0, 65535), + (sitk.sitkUInt8, 0, 0, 0, 0, False), + (sitk.sitkInt16, 0, 0, 0, 0, True), + (sitk.sitkUInt16, 0, 0, 0, 0, False), + ("uint16_uniform", 8191, 57344, 0, 65535, True), + ("uint16_uniform", 8191, 57344, 0, 65535, False), + ("uint8_bimodal", 0, 255, 0, 255, True), + ("uint8_bimodal", -64, 319, 0, 255, False), ], indirect=["image_mrc"], ) -def test_build_histogram_main(image_mrc, expected_min, expected_max, expected_floor, expected_limit): +def test_build_histogram_main(image_mrc, expected_min, expected_max, expected_floor, clamp, expected_limit): runner = CliRunner() output_filename = "out.json" + args = [image_mrc, "--mad", "1.5", "--output-json", output_filename] + if clamp: + args.append("--clamp") + print(args) with runner.isolated_filesystem(): - result = runner.invoke( - pytools.ng.build_histogram.main, [image_mrc, "--mad", "1.5", "--output-json", output_filename] - ) + result = runner.invoke(pytools.ng.build_histogram.main, args=args) assert not result.exception with open(output_filename) as fp: res = json.load(fp)