Example #1
0
    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)
Example #2
0
    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)
Example #3
0
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)
Example #4
0
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)