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':