192 lines
7.2 KiB
Python
192 lines
7.2 KiB
Python
from logging import getLogger
|
|
|
|
from cam_server.pipeline.data_processing import functions
|
|
from cam_server.utils import create_thread_pvs, epics_lock
|
|
|
|
|
|
import json
|
|
|
|
import numpy
|
|
import scipy.signal
|
|
import scipy.optimize
|
|
import numba
|
|
|
|
numba.set_num_threads(4)
|
|
|
|
_logger = getLogger(__name__)
|
|
|
|
channel_names = None
|
|
output_pv, center_pv, fwhm_pv, ymin_pv, ymax_pv, axis_pv = None, None, None, None, None, None
|
|
roi = [0, 0]
|
|
initialized = False
|
|
sent_pid = -1
|
|
|
|
|
|
@numba.njit(parallel=True)
|
|
def get_spectrum(image, background):
|
|
y = image.shape[0]
|
|
x = image.shape[1]
|
|
|
|
profile = numpy.zeros(x, dtype=numpy.uint32)
|
|
|
|
for i in numba.prange(y):
|
|
for j in range(x):
|
|
v = image[i, j]
|
|
b = background[i, j]
|
|
if v > b:
|
|
v -= b
|
|
else:
|
|
#v -= b # added this change to remove +ve noise contribution
|
|
v = 0
|
|
profile[j] += v
|
|
return profile
|
|
|
|
|
|
def initialize(parameters):
|
|
global ymin_pv, ymax_pv, axis_pv, output_pv, center_pv, fwhm_pv
|
|
global channel_names
|
|
epics_pv_name_prefix = parameters["camera_name"]
|
|
output_pv_name = epics_pv_name_prefix + ":SPECTRUM_Y"
|
|
center_pv_name = epics_pv_name_prefix + ":SPECTRUM_CENTER"
|
|
fwhm_pv_name = epics_pv_name_prefix + ":SPECTRUM_FWHM"
|
|
ymin_pv_name = epics_pv_name_prefix + ":SPC_ROI_YMIN"
|
|
ymax_pv_name = epics_pv_name_prefix + ":SPC_ROI_YMAX"
|
|
axis_pv_name = epics_pv_name_prefix + ":SPECTRUM_X"
|
|
channel_names = [output_pv_name, center_pv_name, fwhm_pv_name, ymin_pv_name, ymax_pv_name, axis_pv_name]
|
|
|
|
|
|
def process_image(image, pulse_id, timestamp, x_axis, y_axis, parameters, bsdata=None, background=None):
|
|
global roi, initialized, sent_pid
|
|
global channel_names
|
|
|
|
if not initialized:
|
|
initialize(parameters)
|
|
initialized = True
|
|
[output_pv, center_pv, fwhm_pv, ymin_pv, ymax_pv, axis_pv] = create_thread_pvs(channel_names)
|
|
processed_data = dict()
|
|
epics_pv_name_prefix = parameters["camera_name"]
|
|
|
|
if ymin_pv and ymin_pv.connected:
|
|
roi[0] = ymin_pv.value
|
|
if ymax_pv and ymax_pv.connected:
|
|
roi[1] = ymax_pv.value
|
|
if axis_pv and axis_pv.connected:
|
|
axis = axis_pv.value
|
|
else:
|
|
axis = None
|
|
|
|
if axis is None:
|
|
_logger.warning("Energy axis not connected");
|
|
return None
|
|
|
|
if len(axis) < image.shape[1]:
|
|
_logger.warning("Energy axis length %d < image width %d", len(axis), image.shape[1])
|
|
return None
|
|
|
|
# match the energy axis to image width
|
|
axis = axis[:image.shape[1]]
|
|
|
|
processing_image = image
|
|
nrows, ncols = processing_image.shape
|
|
|
|
# validate background data if passive mode (background subtraction handled here)
|
|
background_image = parameters.pop('background_data', None)
|
|
if isinstance(background_image, numpy.ndarray):
|
|
if background_image.shape != processing_image.shape:
|
|
_logger.info("Invalid background shape: %s instead of %s" % (
|
|
str(background_image.shape), str(processing_image.shape)))
|
|
background_image = None
|
|
else:
|
|
background_image = None
|
|
|
|
processed_data[epics_pv_name_prefix + ":processing_parameters"] = json.dumps(
|
|
{"roi": roi, "background": None if (background_image is None) else parameters.get('image_background')})
|
|
|
|
# crop the image in y direction
|
|
ymin, ymax = int(roi[0]), int(roi[1])
|
|
if nrows >= ymax > ymin >= 0:
|
|
if (nrows != ymax) or (ymin != 0):
|
|
processing_image = processing_image[ymin: ymax, :]
|
|
if background_image is not None:
|
|
background_image = background_image[ymin:ymax, :]
|
|
|
|
# remove the background and collapse in y direction to get the spectrum
|
|
if background_image is not None:
|
|
spectrum = get_spectrum(processing_image, background_image)
|
|
else:
|
|
spectrum = processing_image.sum(0, 'uint32')
|
|
|
|
# smooth the spectrum with savgol filter with 51 window size and 3rd order polynomial
|
|
smoothed_spectrum = scipy.signal.savgol_filter(spectrum, 51, 3)
|
|
|
|
# check wether spectrum has only noise. the average counts per pixel at the peak
|
|
# should be larger than 1.5 to be considered as having real signals.
|
|
minimum, maximum = smoothed_spectrum.min(), smoothed_spectrum.max()
|
|
amplitude = maximum - minimum
|
|
skip = True
|
|
if amplitude > nrows * 1.5:
|
|
skip = False
|
|
# gaussian fitting
|
|
offset, amplitude, center, sigma,perr = gauss_fit_psss2(smoothed_spectrum[::2], axis[::2],
|
|
offset=minimum, amplitude=amplitude, skip=skip, maxfev=20)
|
|
|
|
# outputs
|
|
processed_data[epics_pv_name_prefix + ":SPECTRUM_Y"] = spectrum
|
|
processed_data[epics_pv_name_prefix + ":SPECTRUM_X"] = axis
|
|
processed_data[epics_pv_name_prefix + ":SPECTRUM_CENTER"] = numpy.float64(center)
|
|
processed_data[epics_pv_name_prefix + ":SPECTRUM_FWHM"] = numpy.float64(2.355 * sigma)
|
|
processed_data[epics_pv_name_prefix + ":FIT_ERR"] = perr
|
|
|
|
if epics_lock.acquire(False):
|
|
try:
|
|
if pulse_id > sent_pid:
|
|
sent_pid = pulse_id
|
|
if output_pv and output_pv.connected:
|
|
output_pv.put(processed_data[epics_pv_name_prefix + ":SPECTRUM_Y"])
|
|
#_logger.debug("caput on %s for pulse_id %s", output_pv, pulse_id)
|
|
|
|
if center_pv and center_pv.connected:
|
|
center_pv.put(processed_data[epics_pv_name_prefix + ":SPECTRUM_CENTER"])
|
|
|
|
if fwhm_pv and fwhm_pv.connected:
|
|
fwhm_pv.put(processed_data[epics_pv_name_prefix + ":SPECTRUM_FWHM"])
|
|
finally:
|
|
epics_lock.release()
|
|
|
|
return processed_data
|
|
|
|
##########
|
|
### editted functions
|
|
##########
|
|
|
|
def gauss_fit_psss2(profile, axis, **kwargs):
|
|
if axis.shape[0] != profile.shape[0]:
|
|
raise RuntimeError("Invalid axis passed %d %d" % (axis.shape[0], profile.shape[0]))
|
|
|
|
offset = kwargs.get('offset', profile.min()) # Minimum is good estimation of offset
|
|
amplitude = kwargs.get('amplitude', profile.max() - offset) # Max value is a good estimation of amplitude
|
|
center = kwargs.get('center', numpy.dot(axis,
|
|
profile) / profile.sum()) # Center of mass is a good estimation of center (mu)
|
|
# Consider gaussian integral is amplitude * sigma * sqrt(2*pi)
|
|
standard_deviation = kwargs.get('standard_deviation', numpy.trapz((profile - offset), x=axis) / (
|
|
amplitude * numpy.sqrt(2 * numpy.pi)))
|
|
maxfev = kwargs.get('maxfev', 5) # the default is 100 * (N + 1), which is over killing
|
|
|
|
# If user requests fitting to be skipped, return the estimated parameters.
|
|
if kwargs.get('skip', False):
|
|
return offset, amplitude, center, abs(standard_deviation)
|
|
|
|
try:
|
|
optimal_parameter, pcov = scipy.optimize.curve_fit(
|
|
functions._gauss_function, axis, profile.astype("float64"),
|
|
p0=[offset, amplitude, center, standard_deviation],
|
|
jac=functions._gauss_deriv,
|
|
col_deriv=1,
|
|
maxfev=maxfev)
|
|
offset, amplitude, center, standard_deviation = optimal_parameter
|
|
perr = pcov#numpy.sqrt(numpy.diag(pcov))
|
|
except BaseException as e:
|
|
pass
|
|
|
|
return offset, amplitude, center, abs(standard_deviation), perr
|