From c5db1a25184a6109654ebf833b373e7847d71521 Mon Sep 17 00:00:00 2001 From: turnerm Date: Mon, 18 Mar 2019 11:02:18 +0100 Subject: [PATCH 1/7] Added ZerosTest --- banzai/qc/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/banzai/qc/__init__.py b/banzai/qc/__init__.py index 1d1b36616..c2f5f4758 100644 --- a/banzai/qc/__init__.py +++ b/banzai/qc/__init__.py @@ -3,6 +3,7 @@ from banzai.qc.sinistro_1000s import ThousandsTest from banzai.qc.pattern_noise import PatternNoiseDetector from banzai.qc.header_checker import HeaderSanity +from banzai.qc.zeros import ZerosTest __all__ = ['SaturationTest', 'PointingTest', 'ThousandsTest', - 'PatternNoiseDetector', 'HeaderSanity'] + 'PatternNoiseDetector', 'HeaderSanity', 'ZerosTest'] From c6a9bfbf0606d74137ff16ca034d088b2fdafdb6 Mon Sep 17 00:00:00 2001 From: turnerm Date: Mon, 18 Mar 2019 11:02:54 +0100 Subject: [PATCH 2/7] First commit --- banzai/qc/zeros.py | 41 ++++++++++++++++++++++ banzai/tests/test_zeros_qc.py | 65 +++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 banzai/qc/zeros.py create mode 100644 banzai/tests/test_zeros_qc.py diff --git a/banzai/qc/zeros.py b/banzai/qc/zeros.py new file mode 100644 index 000000000..6e0797683 --- /dev/null +++ b/banzai/qc/zeros.py @@ -0,0 +1,41 @@ +import logging + +import numpy as np + +from banzai.stages import Stage +from banzai.utils import qc + +logger = logging.getLogger(__name__) + + +class ZerosTest(Stage): + """ + Reject any images that have ZEROS_THRESHOLD or more of their pixels exactly equal to 0. + + Notes + ===== + Sometimes when a camera fails, all pixels have a value of 0. + """ + ZEROS_THRESHOLD = 0.2 + + def __init__(self, pipeline_context): + super(ZerosTest, self).__init__(pipeline_context) + + def do_stage(self, image): + npixels = np.product(image.data.shape) + fraction_0s = float(np.sum(image.data == 0)) / npixels + logging_tags = {'FRAC0': fraction_0s, + 'threshold': self.ZEROS_THRESHOLD} + has_0s_error = fraction_0s > self.ZEROS_THRESHOLD + qc_results = {'zeros_test.failed': has_0s_error, + 'zeros_test.fraction': fraction_0s, + 'zeros_testthreshold': self.ZEROS_THRESHOLD} + if has_0s_error: + logger.error('Image is mostly 0s. Rejecting image', image=image, extra_tags=logging_tags) + qc_results['rejected'] = True + return None + else: + logger.info('Measuring fraction of 0s.', image=image, extra_tags=logging_tags) + qc.save_qc_results(self.pipeline_context, qc_results, image) + + return image diff --git a/banzai/tests/test_zeros_qc.py b/banzai/tests/test_zeros_qc.py new file mode 100644 index 000000000..910d91539 --- /dev/null +++ b/banzai/tests/test_zeros_qc.py @@ -0,0 +1,65 @@ +import pytest +import numpy as np + +from banzai.tests.utils import FakeImage +from banzai.qc import ZerosTest + + +@pytest.fixture(scope='module') +def set_random_seed(): + np.random.seed(81232385) + + +def test_null_input_image(): + tester = ZerosTest(None) + image = tester.run(None) + assert image is None + + +def test_no_pixels_0(): + tester = ZerosTest(None) + image = tester.do_stage(FakeImage()) + assert image is not None + + +def test_nonzero_but_no_pixels_0(): + tester = ZerosTest(None) + image = FakeImage() + image.data += 5 + image = tester.do_stage(image) + assert image is not None + + +def test_image_all_0s(): + tester = ZerosTest(None) + image = FakeImage() + image.data += 5 + image.data[:, :] = 0 + image = tester.do_stage(image) + assert image is None + + +def test_image_5_percent_0(set_random_seed): + tester = ZerosTest(None) + nx = 101 + ny = 103 + image = FakeImage(nx=nx, ny=ny) + random_pixels_x = np.random.randint(0, nx - 1, size=int(0.05 * nx * ny)) + random_pixels_y = np.random.randint(0, ny - 1, size=int(0.05 * nx * ny)) + for i in zip(random_pixels_y, random_pixels_x): + image.data[i] = 0 + image = tester.do_stage(image) + assert image is not None + + +def test_image_30_percent_0(set_random_seed): + tester = ZerosTest(None) + nx = 101 + ny = 103 + image = FakeImage(nx=nx, ny=ny) + random_pixels_x = np.random.randint(0, nx - 1, size=int(0.3 * nx * ny)) + random_pixels_y = np.random.randint(0, ny - 1, size=int(0.3 * nx * ny)) + for i in zip(random_pixels_y, random_pixels_x): + image.data[i] = 0 + image = tester.do_stage(image) + assert image is None From a36bc30eb11f7999ccdebc59a865165e69bfb3ce Mon Sep 17 00:00:00 2001 From: turnerm Date: Mon, 18 Mar 2019 13:10:53 +0100 Subject: [PATCH 3/7] Added zeros test to stages list --- banzai/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/banzai/settings.py b/banzai/settings.py index 3d12bb318..e1561d48a 100644 --- a/banzai/settings.py +++ b/banzai/settings.py @@ -82,6 +82,7 @@ class ImagingSettings(Settings): ORDERED_STAGES = [bpm.BPMUpdater, qc.HeaderSanity, + qc.ZerosTest, qc.ThousandsTest, qc.SaturationTest, bias.OverscanSubtractor, From 1d60ad7feb344bff8535d82d8cbd29c16efb4033 Mon Sep 17 00:00:00 2001 From: turnerm Date: Mon, 18 Mar 2019 13:12:42 +0100 Subject: [PATCH 4/7] Changed threshold to 95 percent --- banzai/qc/zeros.py | 2 +- banzai/tests/test_zeros_qc.py | 19 +++---------------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/banzai/qc/zeros.py b/banzai/qc/zeros.py index 6e0797683..97e587439 100644 --- a/banzai/qc/zeros.py +++ b/banzai/qc/zeros.py @@ -16,7 +16,7 @@ class ZerosTest(Stage): ===== Sometimes when a camera fails, all pixels have a value of 0. """ - ZEROS_THRESHOLD = 0.2 + ZEROS_THRESHOLD = 0.95 def __init__(self, pipeline_context): super(ZerosTest, self).__init__(pipeline_context) diff --git a/banzai/tests/test_zeros_qc.py b/banzai/tests/test_zeros_qc.py index 910d91539..fa09dca86 100644 --- a/banzai/tests/test_zeros_qc.py +++ b/banzai/tests/test_zeros_qc.py @@ -39,27 +39,14 @@ def test_image_all_0s(): assert image is None -def test_image_5_percent_0(set_random_seed): +def test_image_95_percent_0(set_random_seed): tester = ZerosTest(None) nx = 101 ny = 103 image = FakeImage(nx=nx, ny=ny) - random_pixels_x = np.random.randint(0, nx - 1, size=int(0.05 * nx * ny)) - random_pixels_y = np.random.randint(0, ny - 1, size=int(0.05 * nx * ny)) + random_pixels_x = np.random.randint(0, nx - 1, size=int(0.95 * nx * ny)) + random_pixels_y = np.random.randint(0, ny - 1, size=int(0.95 * nx * ny)) for i in zip(random_pixels_y, random_pixels_x): image.data[i] = 0 image = tester.do_stage(image) assert image is not None - - -def test_image_30_percent_0(set_random_seed): - tester = ZerosTest(None) - nx = 101 - ny = 103 - image = FakeImage(nx=nx, ny=ny) - random_pixels_x = np.random.randint(0, nx - 1, size=int(0.3 * nx * ny)) - random_pixels_y = np.random.randint(0, ny - 1, size=int(0.3 * nx * ny)) - for i in zip(random_pixels_y, random_pixels_x): - image.data[i] = 0 - image = tester.do_stage(image) - assert image is None From 3d1111fc6d3cc7d5ce843acb909404dedbe46e17 Mon Sep 17 00:00:00 2001 From: turnerm Date: Mon, 18 Mar 2019 14:31:31 +0100 Subject: [PATCH 5/7] Zeros test on individual amps --- banzai/qc/zeros.py | 17 +++++++----- banzai/tests/test_zeros_qc.py | 49 ++++++++++++++++++++++++----------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/banzai/qc/zeros.py b/banzai/qc/zeros.py index 97e587439..5e9ca8624 100644 --- a/banzai/qc/zeros.py +++ b/banzai/qc/zeros.py @@ -10,7 +10,7 @@ class ZerosTest(Stage): """ - Reject any images that have ZEROS_THRESHOLD or more of their pixels exactly equal to 0. + Reject any images that have ZEROS_THRESHOLD or more of their pixels in a single amp exactly equal to 0. Notes ===== @@ -22,14 +22,17 @@ def __init__(self, pipeline_context): super(ZerosTest, self).__init__(pipeline_context) def do_stage(self, image): - npixels = np.product(image.data.shape) - fraction_0s = float(np.sum(image.data == 0)) / npixels - logging_tags = {'FRAC0': fraction_0s, + fraction_0s_list = [] + for i_amp in range(image.get_n_amps()): + npixels = np.product(image.data[i_amp].shape) + fraction_0s_list.append(float(np.sum(image.data[i_amp] == 0)) / npixels) + + has_0s_error = any([fraction_0s > self.ZEROS_THRESHOLD for fraction_0s in fraction_0s_list]) + logging_tags = {'FRAC0': fraction_0s_list, 'threshold': self.ZEROS_THRESHOLD} - has_0s_error = fraction_0s > self.ZEROS_THRESHOLD qc_results = {'zeros_test.failed': has_0s_error, - 'zeros_test.fraction': fraction_0s, - 'zeros_testthreshold': self.ZEROS_THRESHOLD} + 'zeros_test.fraction': fraction_0s_list, + 'zeros_test.threshold': self.ZEROS_THRESHOLD} if has_0s_error: logger.error('Image is mostly 0s. Rejecting image', image=image, extra_tags=logging_tags) qc_results['rejected'] = True diff --git a/banzai/tests/test_zeros_qc.py b/banzai/tests/test_zeros_qc.py index fa09dca86..1581c4bef 100644 --- a/banzai/tests/test_zeros_qc.py +++ b/banzai/tests/test_zeros_qc.py @@ -10,43 +10,62 @@ def set_random_seed(): np.random.seed(81232385) +def get_random_pixel_pairs(nx, ny, fraction): + return np.unravel_index(np.random.choice(range(nx * ny), size=int(fraction * nx * ny)), (ny, nx)) + + def test_null_input_image(): tester = ZerosTest(None) image = tester.run(None) assert image is None -def test_no_pixels_0(): +def test_no_pixels_0_not_rejected(): tester = ZerosTest(None) - image = tester.do_stage(FakeImage()) + image = tester.do_stage(FakeImage(image_multiplier=5)) assert image is not None -def test_nonzero_but_no_pixels_0(): +def test_image_all_0s_rejected(): tester = ZerosTest(None) image = FakeImage() - image.data += 5 + image.data[:, :] = 0 image = tester.do_stage(image) - assert image is not None + assert image is None -def test_image_all_0s(): +def test_image_50_percent_0_not_rejected(set_random_seed): tester = ZerosTest(None) - image = FakeImage() - image.data += 5 - image.data[:, :] = 0 + nx = 101 + ny = 103 + image = FakeImage(nx=nx, ny=ny) + random_pixels = get_random_pixel_pairs(nx, ny, 0.5) + for j,i in zip(random_pixels[0], random_pixels[1]): + image.data[i,i] = 0 image = tester.do_stage(image) - assert image is None + assert image is not None -def test_image_95_percent_0(set_random_seed): +def test_image_99_percent_0_rejected(set_random_seed): tester = ZerosTest(None) nx = 101 ny = 103 image = FakeImage(nx=nx, ny=ny) - random_pixels_x = np.random.randint(0, nx - 1, size=int(0.95 * nx * ny)) - random_pixels_y = np.random.randint(0, ny - 1, size=int(0.95 * nx * ny)) - for i in zip(random_pixels_y, random_pixels_x): + random_pixels = get_random_pixel_pairs(nx, ny, 0.99) + for j,i in zip(random_pixels[0], random_pixels[1]): image.data[i] = 0 image = tester.do_stage(image) - assert image is not None + assert image is None + + +def test_single_amp_99_percent_0_rejected(set_random_seed): + tester = ZerosTest(None) + nx = 101 + ny = 103 + n_amps = 4 + image = FakeImage(nx=nx, ny=ny, n_amps=n_amps) + random_pixels = get_random_pixel_pairs(nx, ny, 0.99) + for j,i in zip(random_pixels[0], random_pixels[1]): + image.data[0][i] = 0 + image = tester.do_stage(image) + assert image is None From c733c2cfe14ed957849a37d4bcf4da5af0386f83 Mon Sep 17 00:00:00 2001 From: turnerm Date: Thu, 28 Mar 2019 13:53:13 +0100 Subject: [PATCH 6/7] Incremented to 0.21.1 --- CHANGES.md | 5 +++++ setup.cfg | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index c45968953..2444377e5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,8 @@ +0.21.1 (2019-03-28) +------------------- +- Added a quality control stage that checks to ensure images are not + primarily comprised of 0s, which indicates a camera failure + 0.21.0 (2019-03-25) ------------------- - Significant refactor to the pipeline context and settings files. We have now diff --git a/setup.cfg b/setup.cfg index 7cb40f051..a8fb273fd 100755 --- a/setup.cfg +++ b/setup.cfg @@ -77,7 +77,7 @@ edit_on_github = True github_project = lcogt/banzai # version should be PEP440 compatible (http://www.python.org/dev/peps/pep-0440) -version = 0.21.0 +version = 0.21.1 [entry_points] banzai = banzai.main:main From 64aae095b775e5454302689c2d29b7368392c7f4 Mon Sep 17 00:00:00 2001 From: turnerm Date: Thu, 28 Mar 2019 14:29:56 +0100 Subject: [PATCH 7/7] Removed pipeline_context --- banzai/qc/zeros.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/banzai/qc/zeros.py b/banzai/qc/zeros.py index 5e9ca8624..127c98725 100644 --- a/banzai/qc/zeros.py +++ b/banzai/qc/zeros.py @@ -18,8 +18,8 @@ class ZerosTest(Stage): """ ZEROS_THRESHOLD = 0.95 - def __init__(self, pipeline_context): - super(ZerosTest, self).__init__(pipeline_context) + def __init__(self, runtime_context): + super(ZerosTest, self).__init__(runtime_context) def do_stage(self, image): fraction_0s_list = [] @@ -39,6 +39,6 @@ def do_stage(self, image): return None else: logger.info('Measuring fraction of 0s.', image=image, extra_tags=logging_tags) - qc.save_qc_results(self.pipeline_context, qc_results, image) + qc.save_qc_results(self.runtime_context, qc_results, image) return image