Commit d536d417 authored by Carsten Richter's avatar Carsten Richter

Merge branch 'merge-roi' into 'master'

Add image ROI during merge

See merge request !60
parents 77140371 16cdd72d
Pipeline #4404 passed with stages
in 3 minutes and 50 seconds
......@@ -78,6 +78,11 @@ chan_per_deg = [318., 318.]
# direct beam position in the detector coordinates
center_chan = [140, 322]
# Detector image ROI to save as (row, column, height, width), example:
# image_roi = [100, 100, 200, 200]
# Set to None to save the whole image:
image_roi = None
# checks if some of the output files already exist
# set it to True if you dont care about overwriting files
overwrite = False
......@@ -92,7 +97,8 @@ merge_scan_data(output_dir,
scan_ids=scan_ids,
img_dir=img_base,
version=version,
overwrite=overwrite)
overwrite=overwrite,
image_roi=image_roi)
t_merge = time.time() - t_merge
print('Total time spent : {0}'.format(t_merge))
......@@ -672,6 +672,47 @@ class MergeWidget(Qt.QDialog):
grpLayout.addWidget(_vLine())
# ###########
# Image ROI
# ###########
self.__imageROIGbx = GroupBox("Image ROI")
self.layout().addWidget(self.__imageROIGbx,
2, 0, Qt.Qt.AlignLeft | Qt.Qt.AlignTop)
layout = Qt.QGridLayout(self.__imageROIGbx)
layout.addWidget(Qt.QLabel('Offset:'), 0, 0)
label = Qt.QLabel('Row:')
label.setToolTip('Row offset from image origin of the image ROI\n'
'to be saved during merge')
layout.addWidget(label, 0, 1)
self.__imageROIRowLineEdit = Qt.QLineEdit('0')
self.__imageROIRowLineEdit.setValidator(Qt.QIntValidator(0, 10000))
layout.addWidget(self.__imageROIRowLineEdit, 0, 2)
label = Qt.QLabel('Column:')
label.setToolTip('Column offset from image origin of the image ROI\n'
'to be saved during merge')
layout.addWidget(label, 0, 3)
self.__imageROIColumnLineEdit = Qt.QLineEdit('0')
self.__imageROIColumnLineEdit.setValidator(Qt.QIntValidator(0, 10000))
layout.addWidget(self.__imageROIColumnLineEdit, 0, 4)
layout.addWidget(Qt.QLabel('Size:'), 1, 0)
label = Qt.QLabel('Height:')
label.setToolTip('Height of the image ROI to be saved during merge\n'
'Default: Whole image')
layout.addWidget(label, 1, 1)
self.__imageROIHeightLineEdit = Qt.QLineEdit()
self.__imageROIHeightLineEdit.setValidator(Qt.QIntValidator(0, 10000))
layout.addWidget(self.__imageROIHeightLineEdit, 1, 2)
label = Qt.QLabel('Width:')
label.setToolTip('Width of the image ROI to be saved during merge\n'
'Default: Whole image')
layout.addWidget(label, 1, 3)
self.__imageROIWidthLineEdit = Qt.QLineEdit()
self.__imageROIWidthLineEdit.setValidator(Qt.QIntValidator(0, 10000))
layout.addWidget(self.__imageROIWidthLineEdit, 1, 4)
# ################
# parameters
# ################
......@@ -680,7 +721,7 @@ class MergeWidget(Qt.QDialog):
self.__acqParamWid = acqParamWid = AcqParamsWidget()
self.layout().addWidget(acqParamsGbx,
2, 0,
3, 0,
Qt.Qt.AlignLeft | Qt.Qt.AlignTop)
grpLayout.addWidget(acqParamWid)
......@@ -691,7 +732,7 @@ class MergeWidget(Qt.QDialog):
self.__outputGbx = outputGbx = GroupBox("Output")
layout = Qt.QGridLayout(outputGbx)
self.layout().addWidget(outputGbx,
3, 0,
4, 0,
Qt.Qt.AlignTop)
# ===========
......@@ -736,7 +777,7 @@ class MergeWidget(Qt.QDialog):
cancelBn = Qt.QPushButton('Cancel')
hLayout = Qt.QHBoxLayout()
self.layout().addLayout(hLayout,
4, 0,
5, 0,
1, 1,
Qt.Qt.AlignHCenter | Qt.Qt.AlignTop)
hLayout.addWidget(mergeBn)
......@@ -905,6 +946,19 @@ class MergeWidget(Qt.QDialog):
raise ValueError('parameter is mandatory.')
merger.output_dir = str(outDir)
name = 'Image ROI'
row = int(self.__imageROIRowLineEdit.text() or '0')
column = int(self.__imageROIColumnLineEdit.text() or '0')
height = self.__imageROIHeightLineEdit.text()
height = int(height) if height else None
width = self.__imageROIWidthLineEdit.text()
width = int(width) if width else None
if row == 0 and column == 0 and height is None and width is None:
merger.image_roi = None # No ROI
else:
merger.image_roi = row, column, height, width
except Exception as ex:
Qt.QMessageBox.critical(self, 'Error',
'{0} : {1}.'.format(name, str(ex)))
......@@ -989,6 +1043,7 @@ class MergeWidget(Qt.QDialog):
- clears the scan widget
"""
self.__scansGbx.setEnabled(False)
self.__imageROIGbx.setEnabled(False)
self.__acqParamsGbx.setEnabled(False)
self.__outputGbx.setEnabled(False)
self.__mergeBn.setEnabled(False)
......@@ -1157,15 +1212,11 @@ class MergeWidget(Qt.QDialog):
enable = True
prefix = merger.prefix
nTotal = len(matched_ids)
nSelected = len(selected_ids)
nNoMatch = len(no_match_ids)
nNoImg = len(no_img_ids)
# Retrieve calibration from selected ids
self.__scansGbx.setEnabled(enable)
self.__acqParamsGbx.setEnabled(len(selected_ids) > 0)
self.__imageROIGbx.setEnabled(len(selected_ids) > 0)
# Set-up default values from first selected scan
if len(selected_ids) > 0:
......@@ -1184,10 +1235,10 @@ class MergeWidget(Qt.QDialog):
if 'cen_pix_y' in calib:
self.__acqParamWid.direct_beam_v = calib['cen_pix_y']
self.__totalScansEdit.setText(str(nTotal))
self.__selectedScansEdit.setText(str(nSelected))
self.__noMatchScansEdit.setText(str(nNoMatch))
self.__noImgInfoEdit.setText(str(nNoImg))
self.__totalScansEdit.setText(str(len(matched_ids)))
self.__selectedScansEdit.setText(str(len(selected_ids)))
self.__noMatchScansEdit.setText(str(len(no_match_ids)))
self.__noImgInfoEdit.setText(str(len(no_img_ids)))
self.__output['prefix'] = prefix
self.__prefixEdit.setText(prefix)
......
......@@ -142,6 +142,17 @@ class XsocsH5(XsocsH5Base):
return self.__detector_params(entry, ['chan_per_deg_dim0',
'chan_per_deg_dim1'])
@_process_entry
def image_roi_offset(self, entry):
"""Image ROI offset that was saved in the hdf5 file
:param str entry: Entry from which to retrieve the information
:return: ROI offset (row_offset, column)
:rtype: Union[List[int],None]
"""
return self.__detector_params(entry, 'image_roi_offset')
@_process_entry
def n_images(self, entry):
# TODO : make sure that data.ndims = 3
......@@ -342,6 +353,15 @@ class XsocsH5Writer(XsocsH5):
'chan_per_deg_dim1': chan_per_deg[1]}
return self.__set_detector_params(entry, value)
def set_image_roi_offset(self, offset, entry):
"""Store image ROI offset information in the hdf5 file
:param List[int] offset:
Offset of the ROI in pixels (row_offset, column_offset)
:param str entry: Entry for which to store information
"""
return self.__set_detector_params(entry, {'image_roi_offset': offset})
def set_scan_params(self,
entry,
motor_0,
......
......@@ -30,6 +30,7 @@ __authors__ = ["D. Naudet"]
__date__ = "20/04/2016"
__license__ = "MIT"
import logging
import re
import copy
import os.path
......@@ -46,6 +47,21 @@ from silx.third_party import EdfFile
from ...io import XsocsH5
_logger = logging.getLogger(__name__)
def divisors(n, start=1):
"""Returns array of divisors of n that are >= start
:param int n: Number for which to find divisors
:param int start: Returns divisors >= to this value
:return: Array of divisors of n
:rtype: numpy.ndarray
"""
divisors_range = np.arange(start, n + 1)
return divisors_range[np.remainder(n, divisors_range) == 0]
class KmapMerger(object):
(READY, RUNNING, DONE,
ERROR, CANCELED, UNKNOWN) = __STATUSES = range(6)
......@@ -121,6 +137,8 @@ class KmapMerger(object):
self.__no_img_ids = sorted(self.__no_img_scans)
self.__on_error_ids = sorted(self.__on_error_scans)
self.__image_roi = None
self.__results = None
self.__master = None
self.__shared_progress = None
......@@ -274,7 +292,8 @@ class KmapMerger(object):
self.beam_energy,
self.chan_per_deg,
self.center_chan,
self.compression)
self.compression,
self.image_roi)
results[scan_id] = pool.apply_async(_add_edf_data,
args,
callback=callback)
......@@ -527,6 +546,23 @@ class KmapMerger(object):
else:
self.__n_proc = n_proc
image_roi = property(lambda self: self.__image_roi)
""" Image ROI (origin_row, origin_column, height, width) to save.
If None (the default) the whole image is saved
"""
@image_roi.setter
def image_roi(self, roi):
if roi is not None:
if len(roi) != 4:
raise ValueError('Image ROI expects 4 positive integers')
for i in roi:
if not isinstance(i, int) or i < 0:
raise ValueError('Image ROI expects 4 positive integers')
self.__image_roi = roi
def get_imagefile_info(self, scan_id, key=None):
try:
......@@ -685,7 +721,8 @@ def _add_edf_data(scan_id,
beam_energy,
chan_per_deg,
center_chan,
compression):
compression,
image_roi):
"""
Creates an entry_*.h5 file with scan data from the provided
......@@ -732,6 +769,7 @@ def _add_edf_data(scan_id,
entry=entry)
if center_chan is not None:
# Write center channel corrected with image ROI offset
entry_h5f.set_direct_beam([float(center_chan[0]),
float(center_chan[1])],
entry=entry)
......@@ -745,8 +783,29 @@ def _add_edf_data(scan_id,
image = edf_file.GetData(0)
dtype = image.dtype
img_shape = image.shape
dset_shape = (n_images, img_shape[0], img_shape[1])
chunks = (1, dset_shape[1]//4, dset_shape[2]//4)
row, column = 0, 0 # Offset in images
if image_roi is not None: # Use ROI and clip it with image shape
row, column, height, width = image_roi
if row >= img_shape[0] or column >= img_shape[1]:
raise ValueError('Image ROI defined outside image')
img_shape = (min(img_shape[0] - row, height),
min(img_shape[1] - column, width))
if img_shape[0] != height or img_shape[1] != width:
_logger.warning(
'Image ROI clipped: It was larger than images')
# Write image roi offset to file
entry_h5f.set_image_roi_offset((row, column), entry=entry)
dset_shape = n_images, img_shape[0], img_shape[1]
chunks = (1,
divisors(dset_shape[1],
start=min(dset_shape[1], 150))[0],
divisors(dset_shape[2],
start=min(dset_shape[2], 150))[0])
with entry_h5f.image_dset_ctx(entry=entry,
create=True,
......@@ -763,7 +822,8 @@ def _add_edf_data(scan_id,
''.format(scan_id))
data = edf_file.GetData(i)
image_dset[i, :, :] = data
image_dset[i, :, :] = data[row:row+img_shape[0],
column:column+img_shape[1]]
except Exception as ex:
print(ex)
......
......@@ -47,7 +47,8 @@ def merge_scan_data(output_dir,
nr_padding=None,
nr_offset=None,
compression='lzf',
overwrite=False):
overwrite=False,
image_roi=None):
"""
Creates a "master" HDF5 file and one HDF5 per scan. Those scan HDF5 files
contain spec data (from *spec_fname*) as well as the associated
......@@ -108,6 +109,11 @@ def merge_scan_data(output_dir,
in the SPEC file.
:type nr_offset: int
:param image_roi:
Detector image ROI (origin_row, origin_column, height, width) to save,
or None (default) to save the whole image
:type image_roi: Union[List[int],None]
:returns: a list of scan IDs that were merged
:rtype: *list*
"""
......@@ -145,6 +151,7 @@ def merge_scan_data(output_dir,
merger.chan_per_deg = chan_per_deg
merger.n_proc = n_proc
merger.compression = compression
merger.image_roi = image_roi
merger.select(scan_ids, clear=True)
......
......@@ -679,6 +679,9 @@ class QSpaceConverter(object):
raise ValueError('Invalid/missing center_chan value : {0}.'
''.format(center_chan))
# Load image ROI from first entry
image_roi_offset = first_param['image_roi_offset']
n_images = first_param['n_images']
if n_images is None or n_images == 0:
raise ValueError(
......@@ -696,8 +699,15 @@ class QSpaceConverter(object):
# Mask is empty, disable mask
mask = None
else:
if (image_roi_offset is not None and
image_roi_offset != (0, 0)):
# Apply image ROI to mask
row, column = image_roi_offset
mask = mask[row[0]:row[0]+img_size[0],
column[1]:column[1]+img_size[1]]
if mask.shape != img_size:
raise ValueError('Invalid mask size')
if (image_binning is not None and
not np.all(np.equal(image_binning, (1, 1)))):
raise ValueError(
......@@ -728,15 +738,21 @@ class QSpaceConverter(object):
n_xy = len(sample_indices)
print('Parameters :')
print('\t- beam energy : {0}'.format(beam_energy))
print('\t- center chan : {0}'.format(center_chan))
print('\t- chan per deg : {0}'.format(chan_per_deg))
print('\t- mask : {0}'.format(
print('\t- beam energy : {0}'.format(beam_energy))
print('\t- center channel : {0}'.format(center_chan))
print('\t- image roi offset : {0}'.format(image_roi_offset))
print('\t- channel per degree : {0}'.format(chan_per_deg))
print('\t- mask : {0}'.format(
'Yes' if mask is not None else 'No'))
print('\t- normalizer : {0}'.format(normalizer))
print('\t- img binning : {0}'.format(image_binning))
print('\t- medfilt dims : {0}'.format(medfilt_dims))
print('\t- qspace size : {0}'.format(qspace_dims))
print('\t- normalizer : {0}'.format(normalizer))
print('\t- img binning : {0}'.format(image_binning))
print('\t- medfilt dims : {0}'.format(medfilt_dims))
print('\t- qspace size : {0}'.format(qspace_dims))
# Offset center_chan with image roi offset if any
if image_roi_offset is not None:
center_chan = (center_chan[0] - image_roi_offset[0],
center_chan[1] - image_roi_offset[1])
# TODO : make this editable?
nx, ny, nz = qspace_dims
......@@ -1549,6 +1565,7 @@ def _get_all_params(data_h5f):
center_chans = []
chan_per_degs = []
angles = []
image_roi_offsets = []
with XsocsH5.XsocsH5(data_h5f, mode='r') as master_h5:
entries = master_h5.entries()
......@@ -1563,6 +1580,7 @@ def _get_all_params(data_h5f):
beam_energy = master_h5.beam_energy(entry=entry)
chan_per_deg = master_h5.chan_per_deg(entry=entry)
center_chan = master_h5.direct_beam(entry=entry)
image_roi_offset = master_h5.image_roi_offset(entry=entry)
angle = master_h5.positioner(entry, 'eta')
......@@ -1572,6 +1590,7 @@ def _get_all_params(data_h5f):
beam_energies.append(beam_energy)
chan_per_degs.append(chan_per_deg)
center_chans.append(center_chan)
image_roi_offsets.append(image_roi_offset)
angles.append(angle)
result = dict([(scan, dict(scans=entries[idx],
......@@ -1581,6 +1600,7 @@ def _get_all_params(data_h5f):
beam_energy=beam_energies[idx],
chan_per_deg=chan_per_degs[idx],
center_chan=center_chans[idx],
image_roi_offset=image_roi_offsets[idx],
angle=angles[idx]))
for idx, scan in enumerate(entries)])
return result
......
......@@ -112,8 +112,38 @@ class TestMerger(unittest.TestCase):
def test_nominal(self):
"""
"""
manager = self._manager
output_dir = os.path.join(self._tmpTestDir, 'merged')
if os.path.isdir(output_dir):
shutil.rmtree(output_dir)
merger = KmapMerger(self._spec_h5,
self._parser.results,
output_dir=output_dir)
merger.center_chan = 140, 322
merger.chan_per_deg = 318, 318
merger.beam_energy = 8000
self.assertEqual(merger.status, merger.READY, msg=merger.statusMsg)
merger.merge()
self.assertEqual(merger.status, merger.DONE, msg=merger.statusMsg)
summary = merger.summary(fullpath=True)
summary_set = set([merged_f
for merged_f in summary.values()])
expected_set = set([os.path.join(output_dir, merged_f)
for merged_f in self.merged_files])
self.assertEqual(summary_set, expected_set)
exists = all([os.path.isfile(merged_f)
for merged_f in expected_set])
self.assertTrue(exists)
def test_image_roi(self):
"""Run merge with image ROI"""
output_dir = os.path.join(self._tmpTestDir, 'merged')
if os.path.isdir(output_dir):
......@@ -126,6 +156,8 @@ class TestMerger(unittest.TestCase):
merger.chan_per_deg = 318, 318
merger.beam_energy = 8000
merger.image_roi = 10, 20, 30, 300
self.assertEqual(merger.status, merger.READY, msg=merger.statusMsg)
merger.merge()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment