def testOrthoReversed(self): grid = DataGrid((GRIDSIZE, GRIDSIZE, GRIDSIZE), np.identity(4)) plane = OrthoSlice(grid, YAXIS, SLICEPOS) # Invert Z axis affine = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, -1, GRIDSIZE - 1], [0, 0, 0, 1]]) datagrid = DataGrid((GRIDSIZE, GRIDSIZE, GRIDSIZE), affine) YD, XD, ZD = np.meshgrid(range(GRIDSIZE), range(GRIDSIZE), range(GRIDSIZE)) xdata, _, _, _ = NumpyData(XD, name="test", grid=datagrid).slice_data(plane) ydata, _, _, _ = NumpyData(YD, name="test", grid=datagrid).slice_data(plane) zdata, _, transv, offset = NumpyData(ZD, name="test", grid=datagrid).slice_data(plane) # Reversal is reflected in the transformation self.assertTrue(np.all(transv == [[1, 0], [0, -1]])) self.assertTrue(np.all(ydata == SLICEPOS)) for x in range(GRIDSIZE): self.assertTrue(np.all(xdata[x, :] == x)) self.assertTrue(np.all(zdata[:, x] == x))
def testHighRes(self): grid = DataGrid((GRIDSIZE, GRIDSIZE, GRIDSIZE), np.identity(4)) plane = OrthoSlice(grid, YAXIS, SLICEPOS) data = np.random.rand(GRIDSIZE * 2, GRIDSIZE * 2, GRIDSIZE * 2) datagrid = DataGrid((GRIDSIZE * 2, GRIDSIZE * 2, GRIDSIZE * 2), np.identity(4) / 2) qpd = NumpyData(data, name="test", grid=datagrid) qpd.slice_data(plane)
def testOrthoZ(self): grid = DataGrid((GRIDSIZE, GRIDSIZE, GRIDSIZE), np.identity(4)) plane = OrthoSlice(grid, ZAXIS, SLICEPOS) self.assertEquals(tuple(plane.origin), (0, 0, SLICEPOS)) self.assertEquals(len(plane.basis), 2) self.assertTrue((0, 1, 0) in plane.basis) self.assertTrue((1, 0, 0) in plane.basis)
def apply_transform(cls, reg_data, transform, options, queue): """ Apply a previously calculated transformation to a data set We are not actually using FSL applyxfm for this although it would be an alternative option for the reference space output option. Instead we perform a non-lossy affine transformation and then resample onto the reference or registration spaces as required. """ log = "Performing non-lossy affine transformation\n" order = options.pop("interp-order", 1) affine = transform.voxel_to_world(reg_data.grid) grid = DataGrid(reg_data.grid.shape, affine) qpdata = NumpyData(reg_data.raw(), grid=grid, name=reg_data.name) output_space = options.pop("output-space", "ref") if output_space == "ref": qpdata = qpdata.resample(transform.ref_grid, suffix="", order=order) log += "Resampling onto reference grid\n" elif output_space == "reg": qpdata = qpdata.resample(transform.reg_grid, suffix="", order=order) log += "Resampling onto input grid\n" return qpdata, log
def testResampleToHiResLinear(self): self.ivm.add(self.data_3d, grid=self.grid, name="data_3d") hires_shape = [dim*2 for dim in self.data_3d.shape] hires_grid = DataGrid(hires_shape, np.identity(4) / 2) hires_data = np.tile(self.data_3d, (2, 2, 2)) self.ivm.add(hires_data, grid=hires_grid, name="hires_data") self.w.order.setCurrentIndex(1) self.w.data.setCurrentIndex(self.w.data.findText("data_3d")) self.w.grid_data.setCurrentIndex(self.w.grid_data.findText("hires_data")) self.processEvents() self.assertEqual(self.w.output_name.value, "data_3d_res") self.w.run.btn.clicked.emit() self.processEvents() self.assertFalse(self.error) self.assertTrue("data_3d_res" in self.ivm.data) # Resampled data should match original data but at twice the resolution self.assertTrue(self.ivm.data["data_3d_res"].grid.matches(hires_grid)) data_res = self.ivm.data["data_3d_res"].raw() X = range(self.data_3d.shape[0]) Y = range(self.data_3d.shape[1]) Z = range(self.data_3d.shape[2]) for x in range(data_res.shape[0]): for y in range(data_res.shape[1]): for z in range(data_res.shape[2]): gx, gy, gz = float(x)/2, float(y)/2, float(z)/2 from scipy.interpolate import interpn d = interpn((X, Y, Z), self.data_3d, (gx, gy, gz), method="linear", bounds_error=False, fill_value=0) self.assertAlmostEqual(data_res[x, y, z], d[0])
def testResampleToHiResNN(self): self.ivm.add(self.data_3d, grid=self.grid, name="data_3d") hires_shape = [dim*2 for dim in self.data_3d.shape] hires_grid = DataGrid(hires_shape, np.identity(4) / 2) hires_data = np.tile(self.data_3d, (2, 2, 2)) self.ivm.add(hires_data, grid=hires_grid, name="hires_data") self.w.order.setCurrentIndex(0) self.w.data.setCurrentIndex(self.w.data.findText("data_3d")) self.w.grid_data.setCurrentIndex(self.w.grid_data.findText("hires_data")) self.processEvents() self.assertEqual(self.w.output_name.value, "data_3d_res") self.w.run.btn.clicked.emit() self.processEvents() self.assertFalse(self.error) self.assertTrue("data_3d_res" in self.ivm.data) # Resampled data should match original data but at twice the resolution self.assertTrue(self.ivm.data["data_3d_res"].grid.matches(hires_grid)) data_res = self.ivm.data["data_3d_res"].raw() for x in range(data_res.shape[0]): for y in range(data_res.shape[1]): for z in range(data_res.shape[2]): nx, ny, nz = int(float(x)/2+0.5), int(float(y)/2+0.5), int(float(z)/2+0.5) if nx < self.data_3d.shape[0] and ny < self.data_3d.shape[1] and nz < self.data_3d.shape[2]: self.assertEqual(data_res[x, y, z], self.data_3d[nx, ny, nz]) else: self.assertEqual(data_res[x, y, z], 0)
def setUp(self): self.shape = [GRIDSIZE, GRIDSIZE, GRIDSIZE] self.grid = DataGrid(self.shape, np.identity(4)) self.floats = np.random.rand(*self.shape) self.ints = np.random.randint(0, 10, self.shape) self.floats4d = np.random.rand(*(self.shape + [ NVOLS, ]))
def testOrthoY(self): grid = DataGrid((GRIDSIZE, GRIDSIZE, GRIDSIZE), np.identity(4)) YD, XD, ZD = np.meshgrid(range(GRIDSIZE), range(GRIDSIZE), range(GRIDSIZE)) plane = OrthoSlice(grid, YAXIS, SLICEPOS) self.assertEquals(tuple(plane.origin), (0, SLICEPOS, 0)) self.assertEquals(len(plane.basis), 2) self.assertTrue((1, 0, 0) in plane.basis) self.assertTrue((0, 0, 1) in plane.basis)
def testAdd(self): shape = [GRIDSIZE, GRIDSIZE, GRIDSIZE] grid = DataGrid(shape, np.identity(4)) qpd = NumpyData(np.random.rand(*shape), name="test", grid=grid) self.ivm.add(qpd) self.assertEqual(len(self.ivm.data), 1) self.assertEqual(len(self.ivm.rois), 0) self.assertTrue(self.ivm.current_data is None) self.assertTrue(self.ivm.current_roi is None) self.assertEqual(self.ivm.main, qpd) self.assertEqual(self.ivm.data["test"], qpd)
def testGenericZ(self): trans = np.array([[0.3, 0.2, 1.7, 0], [0.1, 2.1, 0.11, 0], [2.2, 0.7, 0.3, 0], [0, 0, 0, 1]]) grid = DataGrid((GRIDSIZE, GRIDSIZE, GRIDSIZE), trans) origin = list(SLICEPOS * trans[:3, 2]) plane = OrthoSlice(grid, ZAXIS, SLICEPOS) self.assertAlmostEquals(list(plane.origin), origin) self.assertEquals(len(plane.basis), 2) self.assertTrue(tuple(trans[:3, 0]) in plane.basis) self.assertTrue(tuple(trans[:3, 1]) in plane.basis)
def fslimage_to_qpdata(img, name=None, vol=None, region=None): """ Convert fsl.data.Image to QpData """ if not name: name = img.name if vol is not None: data = img.data[..., vol] else: data = img.data if region is not None: data = (data == region).astype(np.int) return NumpyData(data, grid=DataGrid(img.shape[:3], img.voxToWorldMat), name=name)
def testOrthoOffset(self): grid = DataGrid((GRIDSIZE, GRIDSIZE, GRIDSIZE), np.identity(4)) plane = OrthoSlice(grid, YAXIS, SLICEPOS) # Offset X axis affine = np.array([[1, 0, 0, 2], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) datagrid = DataGrid((GRIDSIZE, GRIDSIZE, GRIDSIZE), affine) YD, XD, ZD = np.meshgrid(range(GRIDSIZE), range(GRIDSIZE), range(GRIDSIZE)) xdata, _, _, _ = NumpyData(XD, name="test", grid=datagrid).slice_data(plane) ydata, _, _, _ = NumpyData(YD, name="test", grid=datagrid).slice_data(plane) zdata, _, transv, offset = NumpyData(ZD, name="test", grid=datagrid).slice_data(plane) self.assertTrue(np.all(ydata == SLICEPOS)) for x in range(GRIDSIZE): self.assertTrue(np.all(xdata[x, :] == x)) self.assertTrue(np.all(zdata[:, x] == x))
def run(self, options): """ Generate test data from Fabber model """ kwargs = { "patchsize": int(math.floor(options.pop("num-voxels", 1000)**(1. / 3) + 0.5)), "nt": options.pop("num-vols", 10), "noise": options.pop("noise", 0), "param_rois": options.pop("save-rois", False), } param_test_values = options.pop("param-test-values", None) output_name = options.pop("output-name", "fabber_test_data") grid_data_name = options.pop("grid", None) if not param_test_values: raise QpException("No test values given for model parameters") api = FabberProcess.api(options.pop("model-group", None)) from fabber import generate_test_data test_data = generate_test_data(api, options, param_test_values, **kwargs) data = test_data["data"] self.debug("Data shape: %s", data.shape) if grid_data_name is None: grid = DataGrid(data.shape[:3], np.identity(4)) else: grid_data = self.ivm.data.get(grid_data_name, None) if grid_data is None: raise QpException("Data not found for output grid: %s" % grid_data_name) grid = grid_data.grid self.ivm.add(data, name=output_name, grid=grid, make_current=True) clean_data = test_data.get("clean", None) if clean_data is not None: self.ivm.add(clean_data, name="%s_clean" % output_name, grid=grid, make_current=False) for param, param_roi in test_data.get("param-rois", {}).items(): self.ivm.add(param_roi, name="%s_roi_%s" % (output_name, param), grid=grid)
def testOrtho(self): grid = DataGrid((GRIDSIZE, GRIDSIZE, GRIDSIZE), np.identity(4)) YD, XD, ZD = np.meshgrid(range(GRIDSIZE), range(GRIDSIZE), range(GRIDSIZE)) plane = OrthoSlice(grid, YAXIS, SLICEPOS) xdata, _, _, _ = NumpyData(XD, name="test", grid=grid).slice_data(plane) ydata, _, _, _ = NumpyData(YD, name="test", grid=grid).slice_data(plane) zdata, _, _, _ = NumpyData(ZD, name="test", grid=grid).slice_data(plane) self.assertTrue(np.all(ydata == SLICEPOS)) for x in range(GRIDSIZE): self.assertTrue(np.all(xdata[x, :] == x)) self.assertTrue(np.all(zdata[:, x] == x))
def reg_3d(cls, reg_data, ref_data, options, queue): """ Static function for performing 3D registration """ from fsl import wrappers as fsl reg = qpdata_to_fslimage(reg_data) ref = qpdata_to_fslimage(ref_data) set_environ(options) output_space = options.pop("output-space", "ref") interp = _interp(options.pop("interp-order", 1)) twod = reg_data.grid.shape[2] == 1 logstream = six.StringIO() flirt_output = fsl.flirt(reg, ref, interp=interp, out=fsl.LOAD, omat=fsl.LOAD, twod=twod, log={ "cmd": logstream, "stdout": logstream, "stderr": logstream }, **options) transform = FlirtTransform(ref_data.grid, flirt_output["omat"], name="flirt_xfm") if output_space == "ref": qpdata = fslimage_to_qpdata(flirt_output["out"], reg_data.name) elif output_space == "reg": qpdata = fslimage_to_qpdata(flirt_output["out"], reg_data.name).resample(reg_data.grid, suffix="") qpdata.name = reg_data.name elif output_space == "trans": trans_affine = transform.voxel_to_world(reg_data.grid) trans_grid = DataGrid(reg_data.grid.shape, trans_affine) qpdata = NumpyData(reg_data.raw(), grid=trans_grid, name=reg_data.name) return qpdata, transform, logstream.getvalue()
def run(self, options): data = self.get_data(options) if data.ndim != 4: raise QpException("Can only simulate motion on 4D data") output_name = options.pop("output-name", "%s_moving" % data.name) std = float(options.pop("std")) std_voxels = [std / size for size in data.grid.spacing] output_grid = data.grid output_shape = data.grid.shape padding = options.pop("padding", 0) if padding > 0: padding_voxels = [ int(math.ceil(padding / size)) for size in data.grid.spacing ] # Need to adjust the origin so the output data lines up with the input output_origin = np.copy(data.grid.origin) output_shape = np.copy(data.grid.shape) output_affine = np.copy(data.grid.affine) for axis in range(3): output_origin[axis] -= np.dot(padding_voxels, data.grid.transform[axis, :]) output_shape[axis] += 2 * padding_voxels[axis] output_affine[:3, 3] = output_origin output_grid = DataGrid(output_shape, output_affine) moving_data = np.zeros(list(output_shape) + [ data.nvols, ]) for vol in range(data.nvols): voldata = data.volume(vol) if padding > 0: voldata = np.pad(voldata, [(v, v) for v in padding_voxels], 'constant', constant_values=0) shift = np.random.normal(scale=std_voxels, size=3) shifted_data = scipy.ndimage.interpolation.shift(voldata, shift) moving_data[..., vol] = shifted_data self.ivm.add(moving_data, grid=output_grid, name=output_name, make_current=True)
def create_test_data(obj, shape=(10, 10, 10), nt=20, motion_scale=0.5): """ Create test data Creates the following attributes on obj, each a Numpy array - grid - data_3d - data_4d - data_4d_moving - mask """ shape = list(shape) centre = [float(v) / 2 for v in shape] obj.grid = DataGrid(shape, np.identity(4)) obj.data_3d = np.zeros(shape, dtype=np.float32) obj.data_4d = np.zeros(shape + [ nt, ], dtype=np.float32) obj.data_4d_moving = np.zeros(shape + [ nt, ], dtype=np.float32) obj.mask = np.zeros(shape, dtype=np.int) for x in range(shape[0]): for y in range(shape[1]): for z in range(shape[2]): nx = 2 * float(x - centre[0]) / shape[0] ny = 2 * float(y - centre[1]) / shape[1] nz = 2 * float(z - centre[2]) / shape[2] d = math.sqrt(nx**2 + ny**2 + nz**2) obj.data_3d[x, y, z] = _test_fn(nx, ny, nz) obj.mask[x, y, z] = int(d < 0.5) for t in range(nt): ft = float(t) / nt obj.data_4d[x, y, z, t] = _test_fn(nx, ny, nz, ft) for t in range(nt): tdata = obj.data_4d[:, :, :, t] shift = np.random.normal(scale=motion_scale, size=3) odata = scipy.ndimage.interpolation.shift(tdata, shift) obj.data_4d_moving[:, :, :, t] = odata
def testAngle45Squiffy(self): affine = np.identity(4) affine[1, 1] = 2 affine[2, 2] = 3 grid = DataGrid(self.data_3d.shape, affine) self.ivm.add(self.data_3d, grid=grid, name="data_3d") self.harmless_click(self.w._angle_btn) self.processEvents() self.ivl._pick(0, (2, 2, 2, 0)) self.processEvents() self.ivl._pick(0, (4, 3, 2, 0)) self.processEvents() self.ivl._pick(0, (2, 3, 2, 0)) self.processEvents() self.assertFalse(self.error) regex = re.compile("angle.*\s+(\d+(\.\d+)?).*", re.IGNORECASE) m = re.match(regex, self.w._label.text()) self.assertTrue(m is not None) self.assertAlmostEquals(float(m.groups()[0]), 45, delta=0.01)
def testAddTwoMixed2(self): shape = [GRIDSIZE, GRIDSIZE, GRIDSIZE] grid = DataGrid(shape, np.identity(4)) qpd = NumpyData(np.random.rand(*shape), name="test", grid=grid) qpd2 = NumpyData(np.random.randint(0, 10, size=shape), name="test2", grid=grid, roi=True) self.assertFalse(qpd.roi) self.assertTrue(qpd2.roi) self.ivm.add(qpd2) self.ivm.add(qpd) self.assertEqual(len(self.ivm.data), 2) self.assertEqual(len(self.ivm.rois), 1) self.assertEqual(self.ivm.main, qpd2) self.assertEqual(self.ivm.current_data, qpd) self.assertTrue(self.ivm.current_roi is None) self.assertEqual(self.ivm.data["test"], qpd) self.assertEqual(self.ivm.data["test2"], qpd2) self.assertEqual(self.ivm.rois["test2"], qpd2)
def testDistance3dNonSquiffy(self): """ Distance with non-squiffy grid """ affine = np.identity(4) affine[1, 1] = 2 affine[2, 2] = 3 grid = DataGrid(self.data_3d.shape, affine) self.ivm.add(self.data_3d, grid=grid, name="data_3d") self.harmless_click(self.w._dist_btn) self.processEvents() self.ivl._pick(0, (2, 2, 2, 0)) self.processEvents() self.ivl._pick(0, (4, 3, 3, 0)) self.processEvents() self.assertFalse(self.error) regex = re.compile("distance.*\s+(\d+(\.\d+)?)\s*mm", re.IGNORECASE) m = re.match(regex, self.w._label.text()) self.assertTrue(m is not None) self.assertAlmostEquals(float(m.groups()[0]), 4.1231, delta=0.01)
def run(self, options): data = self.get_data(options) if data.ndim != 4: raise QpException("Can only simulate motion on 4D data") output_name = options.pop("output-name", "%s_moving" % data.name) std = float(options.pop("std", "0")) std_voxels = [std / size for size in data.grid.spacing] std_degrees = float(options.pop("std_rot", "0")) order = int(options.pop("order", "1")) output_grid = data.grid output_shape = data.grid.shape padding = options.pop("padding", 0) if padding > 0: padding_voxels = [ int(math.ceil(padding / size)) for size in data.grid.spacing ] for dim in range(3): if data.shape[dim] == 1: padding_voxels[dim] = 0 # Need to adjust the origin so the output data lines up with the input output_origin = np.copy(data.grid.origin) output_shape = np.copy(data.grid.shape) output_affine = np.copy(data.grid.affine) for axis in range(3): output_origin[axis] -= np.dot(padding_voxels, data.grid.transform[axis, :]) output_shape[axis] += 2 * padding_voxels[axis] output_affine[:3, 3] = output_origin output_grid = DataGrid(output_shape, output_affine) moving_data = np.zeros(list(output_shape) + [ data.nvols, ]) centre_offset = output_shape / 2 for vol in range(data.nvols): voldata = data.volume(vol) if padding > 0: voldata = np.pad(voldata, [(v, v) for v in padding_voxels], 'constant', constant_values=0) shift = np.random.normal(scale=std_voxels, size=3) for dim in range(3): if voldata.shape[dim] == 1: shift[dim] = 0 shifted_data = scipy.ndimage.shift(voldata, shift, order=order) # Generate random rotation and scale it to the random angle required_angle = np.random.normal(scale=std_degrees, size=1) rot = scipy.spatial.transform.Rotation.random().as_rotvec() rot_angle = np.degrees(np.sqrt(np.sum(np.square(rot)))) rot *= required_angle / rot_angle rot_matrix = scipy.spatial.transform.Rotation.from_rotvec( rot).as_matrix() offset = centre_offset - centre_offset.dot(rot_matrix) rotated_data = scipy.ndimage.affine_transform(shifted_data, rot_matrix.T, offset=offset, order=order) moving_data[..., vol] = rotated_data self.ivm.add(moving_data, grid=output_grid, name=output_name, make_current=True)
def get_simulated_data(self, data_model, param_values, output_param_maps=False): if len(param_values) != 1: raise QpException( "Can only have a single structure in the checkerboard model") param_values = list(param_values.values())[0] param_values_list = {} varying_params = [] for param, values in param_values.items(): if isinstance(values, (int, float)): values = [values] if len(values) > 1: varying_params.append(param) param_values_list[param] = values num_varying_params = len(varying_params) if num_varying_params > 3: raise QpException("At most 3 parameters can vary") elif num_varying_params == 0: # Make it a square for simplicity num_varying_params = 2 voxels_per_patch = self.options.get("voxels-per-patch", 100) side_length = int( round(voxels_per_patch**(1.0 / float(num_varying_params)))) patch_dims = [side_length] * num_varying_params while len(patch_dims) < 3: patch_dims += [ 1, ] repeats = [[0], [0], [0]] checkerboard_dims = [] for idx, param in enumerate(varying_params): num_values = len(param_values_list[param]) repeats[idx] = range(num_values) checkerboard_dims.append(patch_dims[idx] * num_values) for idx in range(len(varying_params), 3): checkerboard_dims.append(patch_dims[idx]) output_data = None import itertools for indexes in itertools.product(*repeats): patch_values = dict(param_values) for idx, param in enumerate(varying_params): patch_values[param] = patch_values[param][indexes[idx]] timeseries = data_model.get_timeseries(patch_values) if output_data is None: output_data = np.zeros(list(checkerboard_dims) + [len(timeseries)], dtype=np.float32) if output_param_maps: param_maps = {} for param in param_values: param_maps[param] = np.zeros(checkerboard_dims, dtype=np.float32) slices = [] for dim_idx, patch_idx in enumerate(indexes): dim_length = patch_dims[dim_idx] slices.append( slice(patch_idx * dim_length, (patch_idx + 1) * dim_length)) output_data[slices] = timeseries if output_param_maps: for param, value in patch_values.items(): param_maps[param][slices] = value grid = DataGrid(checkerboard_dims, np.identity(4)) sim_data = NumpyData(output_data, grid=grid, name="sim_data") if output_param_maps: for param in param_values: param_maps[param] = NumpyData(param_maps[param], grid=grid, name=param) return sim_data, param_maps else: return sim_data
def fslimage_to_qpdata(img, name=None): """ Convert fsl.data.image.Image to QpData """ if not name: name = img.name return NumpyData(img.data, grid=DataGrid(img.shape[:3], img.voxToWorldMat), name=name)
def run(self, options): data = self.get_data(options) if data.roi: default_order = 0 else: default_order = 1 order = options.pop("order", default_order) resample_type = options.pop("type", "data") output_name = options.pop("output-name", "%s_res" % data.name) grid_data = options.pop("grid", None) factor = options.pop("factor", None) only2d = options.pop("2d", None) # The different types of resampling require significantly different strategies # # Data->Data resampling is implemented in the QpData class although this will give # results which are not ideal when resampling to a lower resolution. # Upsampling can use scipy.ndimage.zoom # Downsampling is nore naturally implemented as a mean over subvoxels using Numpy slicing # # Note that factor is an integer for now. It could easily be a float for upsampling but # this would break the downsampling algorithm (and make it significanlty more complex to # implement) # # This is all pretty messy now especially with the '2d only' option. if resample_type == "data": if grid_data is None: raise QpException( "Must provide 'grid' option to specify data item to get target grid from" ) elif grid_data not in self.ivm.data: raise QpException("Data item '%s' not found" % grid_data) grid = self.ivm.data[grid_data].grid output_data = data.resample(grid, order=order) elif resample_type == "up": # Upsampling will need to use interpolation orig_data = data.raw() zooms = [factor for idx in range(3)] if only2d: zooms[2] = 1 if data.ndim == 4: zooms.append(1) output_data = scipy.ndimage.zoom(orig_data, zooms, order=order) # Work out new grid origin voxel_offset = [ float(factor - 1) / (2 * factor) for idx in range(3) ] if only2d: voxel_offset[2] = 0 offset = data.grid.grid_to_world(voxel_offset, direction=True) output_affine = np.array(data.grid.affine) for idx in range(3): if idx < 2 or not only2d: output_affine[:3, idx] /= factor output_affine[:3, 3] -= offset output_grid = DataGrid(output_data.shape[:3], output_affine) output_data = NumpyData(output_data, grid=output_grid, name=output_name) elif resample_type == "down": # Downsampling takes a mean of the voxels inside the new larger voxel # Only uses integral factor at present orig_data = data.raw() new_shape = [ max(1, int(dim_size / factor)) for dim_size in orig_data.shape[:3] ] if data.ndim == 4: new_shape.append(orig_data.shape[3]) if only2d: new_shape[2] = orig_data.shape[2] # Note that output data must be float data type even if original data was integer output_data = np.zeros(new_shape, dtype=np.float32) num_samples = 0 for start1 in range(factor): for start2 in range(factor): for start3 in range(factor): if start1 >= new_shape[ 0] * factor or start2 >= new_shape[ 1] * factor or start3 >= new_shape[ 2] * factor: continue slices = [ slice(start1, new_shape[0] * factor, factor), slice(start2, new_shape[1] * factor, factor), slice(start3, new_shape[2] * factor, factor), ] if only2d: slices[2] = slice(None) downsampled_data = orig_data[slices] output_data += downsampled_data num_samples += 1 output_data /= num_samples # FIXME this will not work for 2D data voxel_offset = [ 0.5 * (factor - 1), 0.5 * (factor - 1), 0.5 * (factor - 1) ] if only2d: voxel_offset[2] = 0 offset = data.grid.grid_to_world(voxel_offset, direction=True) output_affine = np.array(data.grid.affine) for idx in range(3): if idx < 2 or not only2d: output_affine[:3, idx] *= factor output_affine[:3, 3] += offset output_grid = DataGrid(output_data.shape[:3], output_affine) output_data = NumpyData(output_data, grid=output_grid, name=output_name) else: raise QpException("Unknown resampling type: %s" % resample_type) self.ivm.add(output_data, name=output_name, make_current=True, roi=data.roi and order == 0)
class ImageView(QtGui.QSplitter, LogSource): """ Widget containing three orthogonal slice views, two histogram/LUT widgets plus navigation sliders and data summary view. The viewer maintains two main pieces of data: a grid defining the main co-ordinate system of the viewer and a point of focus, in co-ordinates relative to the viewing grid. In addition, the viewer supports 'arrows' to mark positions in space, and variable pickers which control the selection of data. The grid is generally either a straightforward 1mm RAS grid, or an approximate RAS grid derived from the grid of the main data. Although the focus position is provided and set according to this grid by default, the ``focus`` and ``set_focus`` methods allow for the co-ordinates to be set or retrieved according to another arbitrary grid. :ivar grid: Grid the ImageView uses as the basis for the orthogonal slices. This is typically an RAS-aligned version of the main data grid, or alternatively an RAS world-grid """ # Signals when point of focus is changed sig_focus_changed = QtCore.Signal(list) # Signals when the set of marker arrows has changed sig_arrows_changed = QtCore.Signal(list) # Signals when the picker mode is changed sig_picker_changed = QtCore.Signal(object) # Signals when a point is picked. Emission of this signal depends # on the picking mode selected sig_selection_changed = QtCore.Signal(object) def __init__(self, ivm, opts): LogSource.__init__(self) QtGui.QSplitter.__init__(self, QtCore.Qt.Vertical) self.grid = DataGrid([1, 1, 1], np.identity(4)) self._pos = [0, 0, 0, 0] self.ivm = ivm self.opts = opts self.picker = PointPicker(self) self.arrows = [] # Visualisation information for data and ROIs self.main_data_view = MainDataView(self.ivm) self.current_data_view = OverlayView(self.ivm) self.current_roi_view = RoiView(self.ivm) # Navigation controls layout control_box = QtGui.QWidget() vbox = QtGui.QVBoxLayout() vbox.setSpacing(5) control_box.setLayout(vbox) # Create the navigation sliders and the ROI/Overlay view controls vbox.addWidget(DataSummary(self)) hbox = QtGui.QHBoxLayout() nav_box = NavigationBox(self) hbox.addWidget(nav_box) roi_box = RoiViewWidget(self, self.current_roi_view) hbox.addWidget(roi_box) ovl_box = OverlayViewWidget(self, self.current_data_view) hbox.addWidget(ovl_box) vbox.addLayout(hbox) # Histogram which controls colour map and levels for main volume self.main_data_view.histogram = MultiImageHistogramWidget( self, self.main_data_view, percentile=99) # Histogram which controls colour map and levels for data self.current_data_view.histogram = MultiImageHistogramWidget( self, self.current_data_view) # For each view window, this is the volume indices of the x, y and z axes for the view self.ax_map = [[0, 1, 2], [0, 2, 1], [1, 2, 0]] self.ax_labels = [("L", "R"), ("P", "A"), ("I", "S")] # Create three orthogonal views self.ortho_views = {} for i in range(3): win = OrthoView(self, self.ivm, self.ax_map[i], self.ax_labels) win.sig_pick.connect(self._pick) win.sig_drag.connect(self._drag) win.sig_doubleclick.connect(self._toggle_maximise) win.add_data_view(self.main_data_view) win.add_data_view(self.current_data_view) win.add_data_view(self.current_roi_view) self.ortho_views[win.zaxis] = win # Main graphics layout #gview = pg.GraphicsView(background='k') gview = QtGui.QWidget() self.layout_grid = QtGui.QGridLayout() self.layout_grid.setHorizontalSpacing(2) self.layout_grid.setVerticalSpacing(2) self.layout_grid.setContentsMargins(0, 0, 0, 0) self.layout_grid.addWidget( self.ortho_views[1], 0, 0, ) self.layout_grid.addWidget(self.ortho_views[0], 0, 1) self.layout_grid.addWidget(self.main_data_view.histogram, 0, 2) self.layout_grid.addWidget(self.ortho_views[2], 1, 0) self.layout_grid.addWidget(self.current_data_view.histogram, 1, 2) self.layout_grid.setColumnStretch(0, 3) self.layout_grid.setColumnStretch(1, 3) self.layout_grid.setColumnStretch(2, 1) self.layout_grid.setRowStretch(0, 1) self.layout_grid.setRowStretch(1, 1) gview.setLayout(self.layout_grid) self.addWidget(gview) self.addWidget(control_box) self.setStretchFactor(0, 5) self.setStretchFactor(1, 1) self.ivm.sig_main_data.connect(self._main_data_changed) self.opts.sig_options_changed.connect(self._opts_changed) def focus(self, grid=None): """ Get the current focus position :param grid: Report position using co-ordinates relative to this grid. If not specified, report current view grid co-ordinates :return: 4D sequence containing position plus the current data volume index """ if grid is None: return list(self._pos) else: world = self.grid.grid_to_world(self._pos) return list(grid.world_to_grid(world)) def set_focus(self, pos, grid=None): """ Set the current focus position :param grid: Specify position using co-ordinates relative to this grid. If not specified, position is in current view grid co-ordinates """ if grid is not None: world = grid.grid_to_world(pos) pos = self.grid.world_to_grid(world) self._pos = list(pos) if len(self._pos) != 4: raise Exception("Position must be 4D") self.debug("Cursor position: %s", self._pos) self.sig_focus_changed.emit(self._pos) def set_picker(self, pickmode): """ Set the picking mode :param pickmode: Picking mode from :class:`PickMode` """ self.picker.cleanup() self.picker = PICKERS[pickmode](self) self.sig_picker_changed.emit(self.picker) def add_arrow(self, pos, grid=None, col=None): """ Add an arrow to mark a particular position :param pos: Position co-ordinates :param grid: Grid co-ordinates are relative to, if not specified uses viewing grid :param col: Colour as RGB sequence, if not specified uses a default """ if grid is not None: world = grid.grid_to_world(pos) pos = self.grid.world_to_grid(world) if col is None: # Default to grey arrow col = [127, 127, 127] self.arrows.append((pos, col)) self.sig_arrows_changed.emit(self.arrows) def remove_arrows(self): """ Remove all the arrows that have been placed """ self.arrows = [] self.sig_arrows_changed.emit(self.arrows) def capture_view_as_image(self, window, outputfile): """ Export an image using pyqtgraph FIXME this is not working at the moment """ if window not in (1, 2, 3): raise RuntimeError("No such window: %i" % window) expimg = self.ortho_views[window - 1].img exporter = ImageExporter(expimg) exporter.parameters()['width'] = 2000 exporter.export(str(outputfile)) def redraw(self): """ Redraw the view, e.g. on data change This is a hack """ for view in self.ortho_views.values(): view.force_redraw = True self.sig_focus_changed.emit(self._pos) def _pick(self, win, pos): """ Called when a point is picked in one of the viewing windows """ self.picker.pick(win, pos) self.sig_selection_changed.emit(self.picker) def _drag(self, win, pos): """ Called when a drag selection is changed in one of the viewing windows """ self.picker.drag(win, pos) self.sig_selection_changed.emit(self.picker) def _toggle_maximise(self, win, state=-1): """ Maximise/Minimise view window If state=1, maximise, 0=show all, -1=toggle """ win1 = (win + 1) % 3 win2 = (win + 2) % 3 if state == 1 or (state == -1 and self.ortho_views[win1].isVisible()): # Maximise self.layout_grid.addWidget(self.ortho_views[win], 0, 0, 2, 2) self.ortho_views[win1].setVisible(False) self.ortho_views[win2].setVisible(False) self.ortho_views[win].setVisible(True) elif state == 0 or (state == -1 and not self.ortho_views[win1].isVisible()): # Show all three self.layout_grid.addWidget(self.ortho_views[1], 0, 0) self.layout_grid.addWidget(self.ortho_views[0], 0, 1) self.layout_grid.addWidget(self.ortho_views[2], 1, 0) for oview in range(3): self.ortho_views[oview].setVisible(True) self.ortho_views[oview].update() def _opts_changed(self): z_roi = int(self.opts.display_order == self.opts.ROI_ON_TOP) self.current_roi_view.set("z_value", z_roi) self.current_data_view.set("z_value", 1 - z_roi) self.current_data_view.set("interp_order", self.opts.interp_order) def _main_data_changed(self, data): if data is not None: self.grid = data.grid.get_standard() self.debug("Main data raw grid") self.debug(data.grid.affine) self.debug("RAS aligned") self.debug(self.grid.affine) initial_focus = [int(v / 2) for v in data.grid.shape] + [int(data.nvols / 2)] self.debug("Initial focus (data): %s", initial_focus) self.set_focus(initial_focus, grid=data.grid) self.debug("Initial focus (std): %s", self._pos) # If one of the dimensions has size 1 the data is 2D so # maximise the relevant slice self._toggle_maximise(0, state=0) data_axes = data.grid.get_ras_axes() for idx in range(3): self.ortho_views[idx].reset() if data.grid.shape[data_axes[idx]] == 1: self._toggle_maximise(idx, state=1)
def __init__(self, ivm, opts): LogSource.__init__(self) QtGui.QSplitter.__init__(self, QtCore.Qt.Vertical) self.grid = DataGrid([1, 1, 1], np.identity(4)) self._pos = [0, 0, 0, 0] self.ivm = ivm self.opts = opts self.picker = PointPicker(self) self.arrows = [] # Visualisation information for data and ROIs self.main_data_view = MainDataView(self.ivm) self.current_data_view = OverlayView(self.ivm) self.current_roi_view = RoiView(self.ivm) # Navigation controls layout control_box = QtGui.QWidget() vbox = QtGui.QVBoxLayout() vbox.setSpacing(5) control_box.setLayout(vbox) # Create the navigation sliders and the ROI/Overlay view controls vbox.addWidget(DataSummary(self)) hbox = QtGui.QHBoxLayout() nav_box = NavigationBox(self) hbox.addWidget(nav_box) roi_box = RoiViewWidget(self, self.current_roi_view) hbox.addWidget(roi_box) ovl_box = OverlayViewWidget(self, self.current_data_view) hbox.addWidget(ovl_box) vbox.addLayout(hbox) # Histogram which controls colour map and levels for main volume self.main_data_view.histogram = MultiImageHistogramWidget( self, self.main_data_view, percentile=99) # Histogram which controls colour map and levels for data self.current_data_view.histogram = MultiImageHistogramWidget( self, self.current_data_view) # For each view window, this is the volume indices of the x, y and z axes for the view self.ax_map = [[0, 1, 2], [0, 2, 1], [1, 2, 0]] self.ax_labels = [("L", "R"), ("P", "A"), ("I", "S")] # Create three orthogonal views self.ortho_views = {} for i in range(3): win = OrthoView(self, self.ivm, self.ax_map[i], self.ax_labels) win.sig_pick.connect(self._pick) win.sig_drag.connect(self._drag) win.sig_doubleclick.connect(self._toggle_maximise) win.add_data_view(self.main_data_view) win.add_data_view(self.current_data_view) win.add_data_view(self.current_roi_view) self.ortho_views[win.zaxis] = win # Main graphics layout #gview = pg.GraphicsView(background='k') gview = QtGui.QWidget() self.layout_grid = QtGui.QGridLayout() self.layout_grid.setHorizontalSpacing(2) self.layout_grid.setVerticalSpacing(2) self.layout_grid.setContentsMargins(0, 0, 0, 0) self.layout_grid.addWidget( self.ortho_views[1], 0, 0, ) self.layout_grid.addWidget(self.ortho_views[0], 0, 1) self.layout_grid.addWidget(self.main_data_view.histogram, 0, 2) self.layout_grid.addWidget(self.ortho_views[2], 1, 0) self.layout_grid.addWidget(self.current_data_view.histogram, 1, 2) self.layout_grid.setColumnStretch(0, 3) self.layout_grid.setColumnStretch(1, 3) self.layout_grid.setColumnStretch(2, 1) self.layout_grid.setRowStretch(0, 1) self.layout_grid.setRowStretch(1, 1) gview.setLayout(self.layout_grid) self.addWidget(gview) self.addWidget(control_box) self.setStretchFactor(0, 5) self.setStretchFactor(1, 1) self.ivm.sig_main_data.connect(self._main_data_changed) self.opts.sig_options_changed.connect(self._opts_changed)