def test_format_for_filename(self): input_names = [ 'ChannelWith/Slash', 'ChannelWithoutSlash', 'ChannelWithDouble\\Slash', 'NF-\u03BAB' ] expected_names = [ 'ChannelWith-Slash', 'ChannelWithoutSlash', 'ChannelWithDouble-Slash', 'NF-κB' ] formatted_names = [util.format_for_filename(n) for n in input_names] self.assertListEqual(formatted_names, expected_names)
def test_write_single_channel_tiffs(self): basepath = os.path.split(self.filename)[0] tiff.write(basepath, self.image, multichannel=False) for i, (_, channel) in enumerate(CHANNELS): formatted = util.format_for_filename(channel) filename = os.path.join(basepath, '{}.tiff'.format(formatted)) tif = tiff.read(filename) np.testing.assert_equal(np.squeeze(tif.data), DATA[:, :, i]) self.assertTupleEqual(tif.channels, (CHANNELS[i], )) self.assertEqual(tif.data.dtype, np.uint16)
def write(filename, image, sed=None, optical=None, ranges=None, multichannel=True, dtype=None, write_float=None): """Writes MIBI data to a multipage TIFF. Args: filename: The path to the target file if multi-channel, or the path to a folder if single-channel. image: A :class:`mibidata.mibi_image.MibiImage` instance. sed: Optional, an array of the SED image data. This is assumed to be grayscale even if 3-dimensional, in which case only one channel will be used. optical: Optional, an RGB array of the optical image data. ranges: A list of (min, max) tuples the same length as the number of channels. If None, the min will default to zero and the max to the max pixel value in that channel. This is used by some external software to calibrate the display. multichannel: Boolean for whether to create a single multi-channel TIFF, or a folder of single-channel TIFFs. Defaults to True; if False, the sed and optical options are ignored. dtype: dtype: One of (``np.float32``, ``np.uint16``) to force the dtype of the saved image data. Defaults to ``None``, which chooses the format based on the data's input type, and will convert to ``np.float32`` or ``np.uint16`` from other float or int types, respectively, if it can do so without a loss of data. write_float: Deprecated, will raise ValueError if specified. To specify the dtype of the saved image, please use the `dtype` argument instead. Raises: ValueError: Raised if * The image is not a :class:`mibidata.mibi_image.MibiImage` instance. * The :class:`mibidata.mibi_image.MibiImage` coordinates, size, fov_id, fov_name, run, folder, dwell, scans, mass_gain, mass_offset, time_resolution, masses or targets are None. * `dtype` is not one of ``np.float32`` or ``np.uint16``. * `write_float` has been specified. * Converting the native :class:`mibidata.mibi_image.MibiImage` dtype to the specified or inferred ``dtype`` results in a loss of data. """ if not isinstance(image, mi.MibiImage): raise ValueError('image must be a mibidata.mibi_image.MibiImage ' 'instance.') missing_required_metadata = [ m for m in REQUIRED_METADATA_ATTRIBUTES if not getattr(image, m) ] if missing_required_metadata: if len(missing_required_metadata) == 1: missing_metadata_error = (f'{missing_required_metadata[0]} is ' f'required and may not be None.') else: missing_metadata_error = (f'{", ".join(missing_required_metadata)}' f' are required and may not be None.') raise ValueError(missing_metadata_error) if write_float is not None: raise ValueError('`write_float` has been deprecated. Please use the ' '`dtype` argument instead.') if dtype and not dtype in [np.float32, np.uint16]: raise ValueError('Invalid dtype specification.') if dtype == np.float32: save_dtype = np.float32 range_dtype = 'd' elif dtype == np.uint16: save_dtype = np.uint16 range_dtype = 'I' elif np.issubdtype(image.data.dtype, np.floating): save_dtype = np.float32 range_dtype = 'd' else: save_dtype = np.uint16 range_dtype = 'I' to_save = image.data.astype(save_dtype) if not np.all(np.equal(to_save, image.data)): raise ValueError('Cannot convert data from ' f'{image.data.dtype} to {save_dtype}') if ranges is None: ranges = [(0, m) for m in to_save.max(axis=(0, 1))] coordinates = [ (286, '2i', 1, _micron_to_cm(image.coordinates[0])), # x-position (287, '2i', 1, _micron_to_cm(image.coordinates[1])), # y-position ] resolution = (image.data.shape[0] * 1e4 / float(image.size), image.data.shape[1] * 1e4 / float(image.size), 'cm') # The mibi. prefix is added to attributes defined in the spec. # Other user-defined attributes are included too but without the prefix. prefixed_attributes = mi.SPECIFIED_METADATA_ATTRIBUTES[1:] description = {} for key, value in image.metadata().items(): if key in prefixed_attributes: description[f'mibi.{key}'] = value elif key in RESERVED_MIBITIFF_ATTRIBUTES: warnings.warn(f'Skipping writing user-defined {key} to the ' f'metadata as it is a reserved attribute.') elif key != 'date': description[key] = value # TODO: Decide if should filter out those that are None or convert to empty # string so that don't get saved as 'None' if multichannel: targets = list(image.targets) util.sort_channel_names(targets) indices = image.channel_inds(targets) with TiffWriter(filename, software=SOFTWARE_VERSION) as infile: for i in indices: metadata = description.copy() metadata.update({ 'image.type': 'SIMS', 'channel.mass': int(image.masses[i]), 'channel.target': image.targets[i], }) page_name = (285, 's', 0, '{} ({})'.format(image.targets[i], image.masses[i])) min_value = (340, range_dtype, 1, ranges[i][0]) max_value = (341, range_dtype, 1, ranges[i][1]) page_tags = coordinates + [page_name, min_value, max_value] infile.save(to_save[:, :, i], compress=6, resolution=resolution, extratags=page_tags, metadata=metadata, datetime=image.date) if sed is not None: if sed.ndim > 2: sed = sed[:, :, 0] sed_resolution = (sed.shape[0] * 1e4 / float(image.size), sed.shape[1] * 1e4 / float(image.size), 'cm') page_name = (285, 's', 0, 'SED') page_tags = coordinates + [page_name] infile.save(sed, compress=6, resolution=sed_resolution, extratags=page_tags, metadata={'image.type': 'SED'}) if optical is not None: infile.save(optical, compress=6, metadata={'image.type': 'Optical'}) label_coordinates = (_TOP_LABEL_COORDINATES if image.coordinates[1] > 0 else _BOTTOM_LABEL_COORDINATES) slide_label = np.fliplr( np.moveaxis( optical[ label_coordinates[0][0]:label_coordinates[0][1], label_coordinates[1][0]:label_coordinates[1][1]], 0, 1)) infile.save(slide_label, compress=6, metadata={'image.type': 'Label'}) else: for i in range(image.data.shape[2]): metadata = description.copy() metadata.update({ 'image.type': 'SIMS', 'channel.mass': int(image.masses[i]), 'channel.target': image.targets[i], }) page_name = (285, 's', 0, '{} ({})'.format(image.targets[i], image.masses[i])) min_value = (340, range_dtype, 1, ranges[i][0]) max_value = (341, range_dtype, 1, ranges[i][1]) page_tags = coordinates + [page_name, min_value, max_value] target_filename = os.path.join( filename, '{}.tiff'.format(util.format_for_filename(image.targets[i]))) with TiffWriter(target_filename, software=SOFTWARE_VERSION) as infile: infile.save(to_save[:, :, i], compress=6, resolution=resolution, metadata=metadata, datetime=image.date, extratags=page_tags)
def write(filename, image, sed=None, optical=None, ranges=None, multichannel=True, write_float=False): """Writes MIBI data to a multipage TIFF. Args: filename: The path to the target file if multi-channel, or the path to a folder if single-channel. image: A ``mibitof.mibi_image.MibiImage`` instance. sed: Optional, an array of the SED image data. This is assumed to be grayscale even if 3-dimensional, in which case only one channel will be used. optical: Optional, an RGB array of the optical image data. ranges: A list of (min, max) tuples the same length as the number of channels. If None, the min will default to zero and the max to the max pixel value in that channel. This is used by some external software to calibrate the display. multichannel: Boolean for whether to create a single multi-channel TIFF, or a folder of single-channel TIFFs. Defaults to True; if False, the sed and optical options are ignored. write_float: If True, saves the image data as float32 values (for opening properly in certain software such as Halo). Defaults to False which will save the image data as uint16. Note: setting write_float to True does not normalize or scale the data before saving, however saves the integer counts as floating point numbers. Raises: ValueError: Raised if the image is not a ``mibitof.mibi_image.MibiImage`` instance, or if its coordinates run date, or size are None. """ if not isinstance(image, mi.MibiImage): raise ValueError('image must be a mibitof.mibi_image.MibiImage ' 'instance.') if image.coordinates is None or image.size is None: raise ValueError('Image coordinates and size must not be None.') if image.masses is None or image.targets is None: raise ValueError( 'Image channels must contain both masses and targets.') if np.issubdtype(image.data.dtype, np.integer) and not write_float: range_dtype = 'I' else: range_dtype = 'd' if ranges is None: ranges = [(0, m) for m in image.data.max(axis=(0, 1))] coordinates = [ (286, '2i', 1, _motor_to_cm(image.coordinates[0])), # x-position (287, '2i', 1, _motor_to_cm(image.coordinates[1])), # y-position ] resolution = (image.data.shape[0] * 1e4 / float(image.size), image.data.shape[1] * 1e4 / float(image.size), 'cm') metadata = { 'mibi.run': getattr(image, 'run'), 'mibi.version': getattr(image, 'version'), 'mibi.instrument': getattr(image, 'instrument'), 'mibi.slide': getattr(image, 'slide'), 'mibi.dwell': getattr(image, 'dwell'), 'mibi.scans': getattr(image, 'scans'), 'mibi.aperture': getattr(image, 'aperture'), 'mibi.description': getattr(image, 'point_name'), 'mibi.folder': getattr(image, 'folder'), 'mibi.tissue': getattr(image, 'tissue'), 'mibi.panel': getattr(image, 'panel'), 'mibi.mass_offset': getattr(image, 'mass_offset'), 'mibi.mass_gain': getattr(image, 'mass_gain'), 'mibi.time_resolution': getattr(image, 'time_resolution'), 'mibi.miscalibrated': getattr(image, 'miscalibrated'), 'mibi.check_reg': getattr(image, 'check_reg'), 'mibi.filename': getattr(image, 'filename'), } description = { key: val for key, val in metadata.items() if val is not None } if multichannel: targets = list(image.targets) util.sort_channel_names(targets) indices = image.channel_inds(targets) with TiffWriter(filename, software=SOFTWARE_VERSION) as infile: for i in indices: metadata = description.copy() metadata.update({ 'image.type': 'SIMS', 'channel.mass': int(image.masses[i]), 'channel.target': image.targets[i], }) page_name = (285, 's', 0, '{} ({})'.format(image.targets[i], image.masses[i])) min_value = (340, range_dtype, 1, ranges[i][0]) max_value = (341, range_dtype, 1, ranges[i][1]) page_tags = coordinates + [page_name, min_value, max_value] if write_float: to_save = image.data[:, :, i].astype(np.float32) else: to_save = image.data[:, :, i] infile.save(to_save, compress=6, resolution=resolution, extratags=page_tags, metadata=metadata, datetime=image.date) if sed is not None: if sed.ndim > 2: sed = sed[:, :, 0] sed_resolution = (sed.shape[0] * 1e4 / float(image.size), sed.shape[1] * 1e4 / float(image.size), 'cm') page_name = (285, 's', 0, 'SED') page_tags = coordinates + [page_name] infile.save(sed, compress=6, resolution=sed_resolution, extratags=page_tags, metadata={'image.type': 'SED'}) if optical is not None: infile.save(optical, compress=6, metadata={'image.type': 'Optical'}) label_coordinates = (_TOP_LABEL_COORDINATES if image.coordinates[1] > 0 else _BOTTOM_LABEL_COORDINATES) slide_label = np.fliplr( np.moveaxis( optical[ label_coordinates[0][0]:label_coordinates[0][1], label_coordinates[1][0]:label_coordinates[1][1]], 0, 1)) infile.save(slide_label, compress=6, metadata={'image.type': 'Label'}) else: for i in range(image.data.shape[2]): metadata = description.copy() metadata.update({ 'image.type': 'SIMS', 'channel.mass': int(image.masses[i]), 'channel.target': image.targets[i], }) page_name = (285, 's', 0, '{} ({})'.format(image.targets[i], image.masses[i])) min_value = (340, range_dtype, 1, ranges[i][0]) max_value = (341, range_dtype, 1, ranges[i][1]) page_tags = coordinates + [page_name, min_value, max_value] if write_float: target_filename = os.path.join( filename, '{}.float.tiff'.format( util.format_for_filename(image.targets[i]))) else: target_filename = os.path.join( filename, '{}.tiff'.format( util.format_for_filename(image.targets[i]))) with TiffWriter(target_filename, software=SOFTWARE_VERSION) as infile: if write_float: to_save = image.data[:, :, i].astype(np.float32) else: to_save = image.data[:, :, i] infile.save(to_save, compress=6, resolution=resolution, metadata=metadata, datetime=image.date, extratags=page_tags)