From a17010ba66167d681d3dbcbf2d8363d634bb7335 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sun, 23 Jun 2024 22:28:51 +0100 Subject: [PATCH] Add support for mean value binning. This allows the output data to remain 16 bit. --- config/clasp/cam1.json | 1 + config/halfmetre.json | 1 + config/superwasp/cam1.json | 1 + config/superwasp/cam2.json | 1 + config/superwasp/cam3.json | 1 + config/superwasp/cam4.json | 1 + config/warwick.json | 1 + qhy_camd | 8 +++++--- rockit/camera/qhy/client.py | 30 +++++++++++++++++------------- rockit/camera/qhy/config.py | 11 ++++++++--- rockit/camera/qhy/outputprocess.py | 29 ++++++++++++++--------------- rockit/camera/qhy/qhyprocess.py | 16 +++++++++++++--- 12 files changed, 64 insertions(+), 37 deletions(-) diff --git a/config/clasp/cam1.json b/config/clasp/cam1.json index 3ca7689..b7da3b1 100644 --- a/config/clasp/cam1.json +++ b/config/clasp/cam1.json @@ -15,6 +15,7 @@ "gain": 26, "offset": 30, "binning": 1, + "binning_method": "sum", "stream": true, "use_gpsbox": true, "filters": ["NONE"], diff --git a/config/halfmetre.json b/config/halfmetre.json index 7f91c7e..fc3a1c4 100644 --- a/config/halfmetre.json +++ b/config/halfmetre.json @@ -15,6 +15,7 @@ "gain": 26, "offset": 30, "binning": 2, + "binning_method": "sum", "stream": true, "use_gpsbox": true, "filters": ["NONE", "BLOCK", "B", "V", "R"], diff --git a/config/superwasp/cam1.json b/config/superwasp/cam1.json index fa89d67..3ec0eaf 100644 --- a/config/superwasp/cam1.json +++ b/config/superwasp/cam1.json @@ -15,6 +15,7 @@ "gain": 26, "offset": 30, "binning": 1, + "binning_method": "sum", "stream": true, "use_gpsbox": true, "filters": ["RGB-B"], diff --git a/config/superwasp/cam2.json b/config/superwasp/cam2.json index ee454bf..06dfee1 100644 --- a/config/superwasp/cam2.json +++ b/config/superwasp/cam2.json @@ -15,6 +15,7 @@ "gain": 26, "offset": 30, "binning": 1, + "binning_method": "sum", "stream": true, "use_gpsbox": true, "filters": ["RGB-G"], diff --git a/config/superwasp/cam3.json b/config/superwasp/cam3.json index e6313e7..3535d9a 100644 --- a/config/superwasp/cam3.json +++ b/config/superwasp/cam3.json @@ -15,6 +15,7 @@ "gain": 26, "offset": 30, "binning": 1, + "binning_method": "sum", "stream": true, "use_gpsbox": true, "filters": ["RGB-R"], diff --git a/config/superwasp/cam4.json b/config/superwasp/cam4.json index c0a2660..0c714d9 100644 --- a/config/superwasp/cam4.json +++ b/config/superwasp/cam4.json @@ -15,6 +15,7 @@ "gain": 26, "offset": 30, "binning": 1, + "binning_method": "sum", "stream": true, "use_gpsbox": true, "filters": ["i"], diff --git a/config/warwick.json b/config/warwick.json index dc0a8fc..35f3f4b 100644 --- a/config/warwick.json +++ b/config/warwick.json @@ -15,6 +15,7 @@ "gain": 56, "offset": 30, "binning": 3, + "binning_method": "sum", "stream": false, "use_gpsbox": true, "header_card_capacity": 144, diff --git a/qhy_camd b/qhy_camd index 90094bf..1e74d05 100644 --- a/qhy_camd +++ b/qhy_camd @@ -199,7 +199,7 @@ class CameraDaemon: return self.qhy_command('window', window=window, quiet=quiet) @Pyro4.expose - def set_binning(self, binning, quiet=False): + def set_binning(self, binning, method, quiet=False): """Sets the sensor binning factor""" if not pyro_client_matches(self._config.control_ips): return CommandStatus.InvalidControlIP @@ -208,7 +208,7 @@ class CameraDaemon: if not success: return CommandStatus.Blocked - return self.qhy_command('binning', binning=binning, quiet=quiet) + return self.qhy_command('binning', binning=binning, method=method, quiet=quiet) @Pyro4.expose def set_gain(self, gain, quiet=False): @@ -257,6 +257,7 @@ class CameraDaemon: exposure: Exposure time in seconds window: Tuple of 1-indexed (x1, x2, y1, y2) bin: number of pixels to bin in x,y + bin_method: add or mean gain: Gain setting offset: Offset (bias) setting stream: stream (live) exposures or take individual frames @@ -285,7 +286,8 @@ class CameraDaemon: self.qhy_command('window', window=window, quiet=quiet) binning = params.get('bin', self._config.binning) - self.qhy_command('binning', binning=binning, quiet=quiet) + method = params.get('bin_method', self._config.binning_method) + self.qhy_command('binning', binning=binning, method=method, quiet=quiet) gain = params.get('gain', self._config.gain) self.qhy_command('gain', gain=gain, quiet=quiet) diff --git a/rockit/camera/qhy/client.py b/rockit/camera/qhy/client.py index 3e81416..08d6806 100644 --- a/rockit/camera/qhy/client.py +++ b/rockit/camera/qhy/client.py @@ -55,6 +55,8 @@ def run_client_command(config_path, usage_prefix, args): print('warm') elif 'window' in args[-2:]: print('default') + elif 'bin' in args[-3:-1]: + print('add mean') elif len(args) < 3: print(' '.join(commands)) return 0 @@ -110,7 +112,7 @@ def status(config, *_): w = [x + 1 for x in data['window']] print(f' Output window is {TFmt.Bold}[{w[0]}:{w[1]},{w[2]}:{w[3]}]{TFmt.Clear}') - print(f' Binning is {TFmt.Bold}{data["binning"]}x{data["binning"]}{TFmt.Clear}') + print(f' Binning is {TFmt.Bold}{data["binning"]}x{data["binning"]}{TFmt.Clear} ({TFmt.Bold}{data["binning_method"]}{TFmt.Clear})') if data['filter']: print(f' Filter is {TFmt.Bold}{data["filter"]}{TFmt.Clear}') return 0 @@ -170,20 +172,22 @@ def set_window(config, usage_prefix, args): def set_binning(config, usage_prefix, args): """Set the camera binning""" - if len(args) == 1: - binning = None - if args[0] != 'default': - try: - binning = int(args[0]) - except ValueError: - print(f'usage: {usage_prefix} bin ') - return -1 + if len(args) == 1 and args[0] == 'default': + binning = method = None + elif len(args) == 2 and args[1] in ['sum', 'mean']: + try: + binning = int(args[0]) + except ValueError: + print(f'usage: {usage_prefix} bin (sum|mean)') + return -1 + method = args[1] + else: + print(f'usage: {usage_prefix} bin (sum|mean)') + return -1 - with config.daemon.connect() as camd: - return camd.set_binning(binning) + with config.daemon.connect() as camd: + return camd.set_binning(binning, method) - print(f'usage: {usage_prefix} bin ') - return -1 def set_streaming(config, usage_prefix, args): diff --git a/rockit/camera/qhy/config.py b/rockit/camera/qhy/config.py index 8b56ba5..3c93d85 100644 --- a/rockit/camera/qhy/config.py +++ b/rockit/camera/qhy/config.py @@ -26,9 +26,9 @@ 'additionalProperties': False, 'required': [ 'daemon', 'pipeline_daemon', 'pipeline_handover_timeout', 'log_name', 'control_machines', - 'client_commands_module', - 'camera_device_id', 'camera_id', 'cooler_setpoint', 'cooler_update_delay', 'cooler_pwm_step', - 'worker_processes', 'framebuffer_bytes', 'mode', 'gain', 'offset', 'binning', 'stream', 'use_gpsbox', + 'client_commands_module', 'camera_device_id', 'camera_id', + 'cooler_setpoint', 'cooler_update_delay', 'cooler_pwm_step', 'worker_processes', + 'framebuffer_bytes', 'mode', 'gain', 'offset', 'binning', 'binning_method', 'stream', 'use_gpsbox', 'header_card_capacity', 'output_path', 'output_prefix', 'expcount_path' ], 'properties': { @@ -101,6 +101,10 @@ 'min': 1, 'max': 9600, }, + 'binning_method': { + 'type': 'string', + 'enum': ['sum', 'mean'], + }, 'stream': { 'type': 'boolean', }, @@ -164,6 +168,7 @@ def __init__(self, config_filename): self.gain = config_json['gain'] self.offset = config_json['offset'] self.binning = config_json['binning'] + self.binning_method = config_json['binning_method'] self.stream = config_json['stream'] self.use_gpsbox = config_json['use_gpsbox'] self.filters = config_json.get('filters', []) diff --git a/rockit/camera/qhy/outputprocess.py b/rockit/camera/qhy/outputprocess.py index 053cbc3..60e11c3 100644 --- a/rockit/camera/qhy/outputprocess.py +++ b/rockit/camera/qhy/outputprocess.py @@ -26,6 +26,7 @@ from astropy.time import Time import astropy.units as u import numpy as np +from numpy.lib.stride_tricks import as_strided from rockit.common import daemons, log from .constants import CoolerMode @@ -201,25 +202,23 @@ def output_process(process_queue, processing_framebuffer, processing_framebuffer data = data[window_region[2]:window_region[3] + 1, window_region[0]:window_region[1] + 1] if frame['binning'] > 1: - nrows, ncols = data.shape - n_binned_cols = ncols//frame['binning'] - n_binned_rows = nrows//frame['binning'] - binned_cols = np.zeros((nrows, n_binned_cols), dtype=np.uint32) - - for i in range(nrows): - binned_cols[i] = np.sum(data[i][:n_binned_cols*frame['binning']] - .reshape(n_binned_cols, frame['binning']), axis=1) - - data = np.zeros((n_binned_rows, n_binned_cols), dtype=np.uint32) - for i in range(n_binned_cols): - data[:, i] = np.sum(binned_cols[:, i][:n_binned_rows*frame['binning']] - .reshape(n_binned_rows, frame['binning']), axis=1) + # Create a 4d view of the 2d image, where the two new axes + # correspond to the pixels that are to be binned in the x and y axes + binning = np.array((frame['binning'], frame['binning'])) + blocked_shape = tuple(data.shape // binning) + tuple(binning) + blocked_strides = tuple(data.strides * binning) + data.strides + blocked = as_strided(data, shape=blocked_shape, strides=blocked_strides) + + if frame['binning_method'] == 'sum': + data = np.sum(blocked, axis=(2, 3), dtype=np.uint32) + else: + data = np.mean(blocked, axis=(2, 3), dtype=np.uint16) image_region = bin_sensor_region(image_region, frame['binning']) bias_region = bin_sensor_region(bias_region, frame['binning']) dark_region = bin_sensor_region(dark_region, frame['binning']) - window_region[1] = frame['window_region'][0] + n_binned_cols * frame['binning'] - 1 - window_region[3] = frame['window_region'][2] + n_binned_rows * frame['binning'] - 1 + window_region[1] = frame['window_region'][0] + blocked_shape[1] * frame['binning'] - 1 + window_region[3] = frame['window_region'][2] + blocked_shape[0] * frame['binning'] - 1 if image_region is not None: image_region_header = ('IMAG-RGN', format_sensor_region(image_region), diff --git a/rockit/camera/qhy/qhyprocess.py b/rockit/camera/qhy/qhyprocess.py index ff1b552..0475a3a 100644 --- a/rockit/camera/qhy/qhyprocess.py +++ b/rockit/camera/qhy/qhyprocess.py @@ -111,6 +111,7 @@ def __init__(self, config, processing_queue, # Crop output data to detector coordinates self._window_region = [0, 0, 0, 0] self._binning = config.binning + self._binning_method = config.binning_method # Image geometry (marking edges of overscan etc) self._image_region = [0, 0, 0, 0] @@ -410,6 +411,7 @@ def __run_exposure_sequence(self, quiet): 'cooler_setpoint': self._cooler_setpoint, 'window_region': self._window_region, 'binning': self._binning, + 'binning_method': self._binning_method, 'image_region': self._image_region, 'bias_region': self._bias_region, 'dark_region': self._dark_region, @@ -773,7 +775,7 @@ def set_window(self, window, quiet): else: return CommandStatus.Failed - def set_binning(self, binning, quiet): + def set_binning(self, binning, method, quiet): """Set the camera binning""" if self.is_acquiring: return CommandStatus.CameraNotIdle @@ -781,13 +783,20 @@ def set_binning(self, binning, quiet): if binning is None: binning = self._config.binning + if method is None: + method = self._config.binning_method + if not isinstance(binning, int) or binning < 1: return CommandStatus.Failed + if method not in ['sum', 'mean']: + return CommandStatus.Failed + self._binning = binning + self._binning_method = method if not quiet: - log.info(self._config.log_name, f'Binning set to {binning}') + log.info(self._config.log_name, f'Binning set to {binning} ({method})') return CommandStatus.Succeeded @@ -926,6 +935,7 @@ def report_status(self): 'exposure_progress': exposure_progress, 'window': self._window_region, 'binning': self._binning, + 'binning_method': self._binning_method, 'sequence_frame_limit': self._sequence_frame_limit, 'sequence_frame_count': sequence_frame_count, 'filter': self._filter, @@ -986,7 +996,7 @@ def qhy_process(camd_pipe, config, elif command == 'window': camd_pipe.send(cam.set_window(args['window'], args['quiet'])) elif command == 'binning': - camd_pipe.send(cam.set_binning(args['binning'], args['quiet'])) + camd_pipe.send(cam.set_binning(args['binning'], args['method'], args['quiet'])) elif command == 'start': camd_pipe.send(cam.start_sequence(args['count'], args['quiet'])) elif command == 'stop':