Ejemplo n.º 1
0
def _format_volume_to_header(volume: MedicalVolume) -> MedicalVolume:
    """Reformats the volume according to its header.

    Args:
        volume (MedicalVolume): The volume to reformat.
            Must be 3D and have headers of shape (1, 1, volume.shape[2]).

    Returns:
        MedicalVolume: The reformatted volume.
    """
    headers = volume.headers()
    assert headers.shape == (1, 1, volume.shape[2])

    affine = to_RAS_affine(headers.flatten())
    orientation = stdo.orientation_nib_to_standard(nib.aff2axcodes(affine))

    # Currently do not support mismatch in scanner_origin.
    if tuple(affine[:3, 3]) != volume.scanner_origin:
        raise ValueError(
            "Scanner origin mismatch. "
            "Currently we do not handle mismatch in scanner origin "
            "(i.e. cannot flip across axis)")

    volume = volume.reformat(orientation)
    assert volume.headers().shape == (1, 1, volume.shape[2])
    return volume
Ejemplo n.º 2
0
    def test_metadata(self):
        field, field_val = "EchoTime", 4.0

        volume = np.random.rand(10, 20, 30, 40)
        headers = ututils.build_dummy_headers(volume.shape[2:],
                                              {field: field_val})
        mv_no_headers = MedicalVolume(volume, self._AFFINE)
        mv = MedicalVolume(volume, self._AFFINE, headers=headers)

        assert mv_no_headers.headers() is None
        assert mv_no_headers.headers(flatten=True) is None

        with self.assertRaises(ValueError):
            mv.get_metadata("foobar")
        assert mv.get_metadata("foobar", default=0) == 0

        echo_time = mv.get_metadata(field)
        assert echo_time == field_val

        new_val = 5.0
        mv2 = mv.clone(headers=True)
        mv2.set_metadata(field, new_val)
        assert mv.get_metadata(field, type(field_val)) == field_val
        assert mv2.get_metadata(field, type(new_val)) == new_val
        for h in mv2.headers(flatten=True):
            assert h[field].value == new_val

        new_val = 6.0
        mv2 = mv.clone(headers=True)
        mv2[..., 1].set_metadata(field, new_val)
        assert mv2[..., 0].get_metadata(field) == field_val
        assert mv2[..., 1].get_metadata(field) == new_val
        headers = mv2.headers()
        for h in headers[..., 0].flatten():
            assert h[field].value == field_val
        for h in headers[..., 1].flatten():
            assert h[field].value == new_val

        # Set metadata when volume has no headers.
        mv_nh = MedicalVolume(volume, self._AFFINE, headers=None)
        with self.assertRaises(ValueError):
            mv_nh.set_metadata("EchoTime", 40.0)
        with self.assertWarns(UserWarning):
            mv_nh.set_metadata("EchoTime", 40.0, force=True)
        assert mv_nh._headers.shape == (1, ) * len(mv_nh.shape)
        assert mv_nh.get_metadata("EchoTime") == 40.0
        assert mv_nh[:1, :2, :3]._headers.shape == (1, ) * len(mv_nh.shape)
Ejemplo n.º 3
0
    def test_clone(self):
        mv = MedicalVolume(np.random.rand(10, 20, 30), self._AFFINE)
        mv2 = mv.clone()
        assert mv.is_identical(mv2)  # expected identical volumes

        mv = MedicalVolume(
            np.random.rand(10, 20, 30),
            self._AFFINE,
            headers=ututils.build_dummy_headers((1, 1, 30)),
        )
        mv2 = mv.clone(headers=False)
        assert mv.is_identical(mv2)  # expected identical volumes
        assert id(mv.headers(flatten=True)[0]) == id(
            mv2.headers(flatten=True)
            [0]), "headers not cloned, expected same memory address"

        mv3 = mv.clone(headers=True)
        assert mv.is_identical(mv3)  # expected identical volumes
        assert id(mv.headers(flatten=True)[0]) != id(
            mv3.headers(flatten=True)
            [0]), "headers cloned, expected different memory address"
Ejemplo n.º 4
0
    def save(
        self,
        volume: MedicalVolume,
        dir_path: str,
        fname_fmt: str = np._NoValue,
        sort_by: Union[str, int, Sequence[Union[str, int]]] = np._NoValue,
    ):
        """Save `medical volume` in dicom format.

        This function assumes headers for the volume (``volume.headers()``) exist
        for one spatial dimension. Headers for non-spatial dimensions are optional, but
        highly recommended. If provided, they will be used to write the volume. If not,
        headers will be appropriately broadcast to these dimensions. Note, this means
        that multiple files will have the same header information and will not be able
        to be loaded automatically.

        Currently header spatial information (orientation, origin, slicing between spaces,
        etc.) is not overwritten nor validated. All data must correspond to the same
        spatial information as specified in the headers to produce valid DICOM files.

        Args:
            volume (MedicalVolume): Volume to save.
            dir_path: Directory path to store dicom files. Dicoms are stored in directories,
                as multiple files are needed to store the volume.
            fname_fmt (str, optional): Formatting string for filenames. Must contain ``%d``,
                which correspopnds to slice number. Defaults to ``self.fname_fmt``.
            sort_by (``str``(s) or ``int``(s), optional): DICOM attribute(s) used
                to define ordering of slices prior to writing. If ``None``, this ordering
                will be defined by the order of blocks in ``volume``. Defaults to
                ``self.sort_by``.

        Raises:
            ValueError: If `im` does not have initialized headers. Or if `im` was flipped across
                any axis. Flipping changes scanner origin, which is currently not handled.
        """
        fname_fmt = fname_fmt if fname_fmt != np._NoValue else self.fname_fmt
        sort_by = sort_by if sort_by != np._NoValue else self.sort_by

        # Get orientation indicated by headers.
        headers = volume.headers()
        if headers is None:
            raise ValueError(
                "MedicalVolume headers must be initialized to save as a dicom")

        sort_by = _wrap_as_tuple(sort_by, default=())

        # Reformat to put headers in last dimensions.
        single_dim = []
        full_dim = []
        for i, dim in enumerate(headers.shape[:3]):
            if dim == 1:
                single_dim.append(i)
            else:
                full_dim.append(i)
        if len(full_dim) > 1:
            raise ValueError(
                f"Only one spatial dimension can have headers. Got {len(full_dim)} - "
                f"headers.shape={headers.shape[:3]}")
        new_orientation = (volume.orientation[x]
                           for x in single_dim + full_dim)

        volume = volume.reformat(new_orientation)
        assert volume.headers().shape[:3] == (1, 1, volume.shape[2])

        # Reformat medical volume to expected orientation specified by dicom headers.
        # NOTE: This is temporary. Future fixes will allow us to modify header
        # data to match affine matrix.
        if len(volume.shape) > 3:
            shape = volume.shape[3:]
            multi_volumes = np.empty(shape, dtype=object)
            for dims in itertools.product(
                    *[list(range(0, x)) for x in multi_volumes.shape]):
                multi_volumes[dims] = _format_volume_to_header(
                    volume[(Ellipsis, ) + dims])
            multi_volumes = multi_volumes.flatten()
            volume_arr = np.concatenate([v.volume for v in multi_volumes],
                                        axis=-1)
            headers = np.concatenate(
                [v.headers(flatten=True) for v in multi_volumes], axis=-1)
        else:
            volume = _format_volume_to_header(volume)
            volume_arr = volume.volume
            headers = volume.headers(flatten=True)

        assert headers.ndim == 1
        assert volume_arr.shape[2] == len(
            headers
        ), "Dimension mismatch - {:d} slices but {:d} headers".format(
            volume_arr.shape[-1], len(headers))

        if sort_by:
            idxs = np.asarray(
                index_natsorted(
                    headers,
                    key=lambda h: tuple(
                        _unpack_dicom_attr(h, k, required=True)
                        for k in sort_by),
                ))
            headers = headers[idxs]
            volume_arr = volume_arr[..., idxs]

        # Check if dir_path exists.
        os.makedirs(dir_path, exist_ok=True)

        num_slices = len(headers)
        if not fname_fmt:
            filename_format = "I%0" + str(max(4, ceil(
                log10(num_slices)))) + "d.dcm"
        else:
            filename_format = fname_fmt

        filepaths = [
            os.path.join(dir_path, filename_format % (s + 1))
            for s in range(num_slices)
        ]
        if self.num_workers:
            slices = [volume_arr[..., s] for s in range(num_slices)]
            if self.verbose:
                process_map(_write_dicom_file, slices, headers, filepaths)
            else:
                with mp.Pool(self.num_workers) as p:
                    out = p.starmap_async(_write_dicom_file,
                                          zip(slices, headers, filepaths))
                    out.wait()
        else:
            for s in tqdm(range(num_slices), disable=not self.verbose):
                _write_dicom_file(volume_arr[..., s], headers[s], filepaths[s])
Ejemplo n.º 5
0
    def test_numpy(self):
        mv = MedicalVolume(np.ones((10, 20, 30)), np.eye(4))
        assert np.all(np.exp(mv.volume) == np.exp(mv).volume)

        mv[np.where(mv == 1)] = 5
        assert np.all(mv == 5)
        assert not np.any(mv == 1)
        assert all(mv == 5)
        assert not any(mv == 5)

        _ = mv + np.ones(mv.shape)

        shape = (10, 20, 30, 2)
        headers = np.stack(
            [
                ututils.build_dummy_headers(shape[2], {"EchoTime": 2}),
                ututils.build_dummy_headers(shape[2], {"EchoTime": 10}),
            ],
            axis=-1,
        )

        # Reduce functions
        mv = MedicalVolume(np.random.rand(*shape), np.eye(4), headers=headers)

        mv2 = np.add.reduce(mv, -1)
        assert np.all(mv2 == np.add.reduce(mv.volume, axis=-1))
        assert mv2.shape == mv.shape[:3]
        mv2 = np.add.reduce(mv, axis=None)
        assert np.all(mv2 == np.add.reduce(mv.volume, axis=None))
        assert np.isscalar(mv2)

        mv2 = np.sum(mv, axis=-1)
        assert np.all(mv2 == np.sum(mv.volume, axis=-1))
        assert mv2.shape == mv.shape[:3]
        mv2 = np.sum(mv)
        assert np.all(mv2 == np.sum(mv.volume))
        assert np.isscalar(mv2)

        mv2 = mv.sum(axis=-1)
        assert np.all(mv2 == np.sum(mv.volume, axis=-1))
        assert mv2.shape == mv.shape[:3]
        mv2 = mv.sum()
        assert np.all(mv2 == np.sum(mv.volume))
        assert np.isscalar(mv2)

        mv2 = np.mean(mv, axis=-1)
        assert np.all(mv2 == np.mean(mv.volume, axis=-1))
        assert mv2.shape == mv.shape[:3]
        mv2 = np.mean(mv)
        assert np.all(mv2 == np.mean(mv.volume))
        assert np.isscalar(mv2)

        mv2 = mv.mean(axis=-1)
        assert np.all(mv2 == np.mean(mv.volume, axis=-1))
        assert mv2.shape == mv.shape[:3]
        mv2 = mv.mean()
        assert np.all(mv2 == np.mean(mv.volume))
        assert np.isscalar(mv2)

        mv2 = np.std(mv, axis=-1)
        assert np.all(mv2 == np.std(mv.volume, axis=-1))
        assert mv2.shape == mv.shape[:3]
        mv2 = np.std(mv)
        assert np.all(mv2 == np.std(mv.volume))
        assert np.isscalar(mv2)

        # Min/max functions
        mv2 = np.amin(mv, axis=-1)
        assert np.all(mv2 == np.amin(mv.volume, axis=-1))
        assert mv2.shape == mv.shape[:3]
        mv2 = np.amin(mv)
        assert np.all(mv2 == np.amin(mv.volume))
        assert np.isscalar(mv2)

        mv2 = np.amax(mv, axis=-1)
        assert np.all(mv2 == np.amax(mv.volume, axis=-1))
        assert mv2.shape == mv.shape[:3]
        mv2 = np.amax(mv)
        assert np.all(mv2 == np.amax(mv.volume))
        assert np.isscalar(mv2)

        mv2 = np.argmin(mv, axis=-1)
        assert np.all(mv2 == np.argmin(mv.volume, axis=-1))
        assert mv2.shape == mv.shape[:3]
        mv2 = np.argmin(mv)
        assert np.all(mv2 == np.argmin(mv.volume))

        mv2 = np.argmax(mv, axis=-1)
        assert np.all(mv2 == np.argmax(mv.volume, axis=-1))
        assert mv2.shape == mv.shape[:3]
        mv2 = np.argmax(mv)
        assert np.all(mv2 == np.argmax(mv.volume))

        # NaN functions
        vol_nan = np.ones(shape)
        vol_nan[..., 1] = np.nan
        mv = MedicalVolume(vol_nan, np.eye(4), headers=headers)

        mv2 = np.nansum(mv, axis=-1)
        assert np.all(mv2 == np.nansum(mv.volume, axis=-1))
        assert mv2.shape == mv.shape[:3]
        mv2 = np.nansum(mv)
        assert np.all(mv2 == np.nansum(mv.volume))
        assert np.isscalar(mv2)

        mv2 = np.nanmean(mv, axis=-1)
        assert np.all(mv2 == np.nanmean(mv.volume, axis=-1))
        assert mv2.shape == mv.shape[:3]
        mv2 = np.nanmean(mv)
        assert np.all(mv2 == np.nanmean(mv.volume))
        assert np.isscalar(mv2)

        mv2 = np.nanstd(mv, axis=-1)
        assert np.all(mv2 == np.nanstd(mv.volume, axis=-1))
        assert mv2.shape == mv.shape[:3]
        mv2 = np.nanstd(mv)
        assert np.all(mv2 == np.nanstd(mv.volume))
        assert np.isscalar(mv2)

        mv2 = np.nan_to_num(mv)
        assert np.unique(mv2.volume).tolist() == [0, 1]
        mv2 = np.nan_to_num(mv, copy=False)
        assert id(mv2) == id(mv)

        mv2 = np.nanmin(mv, axis=-1)
        assert np.all(mv2 == np.nanmin(mv.volume, axis=-1))
        assert mv2.shape == mv.shape[:3]
        mv2 = np.nanmin(mv)
        assert np.all(mv2 == np.nanmin(mv.volume))
        assert np.isscalar(mv2)

        mv2 = np.nanmax(mv, axis=-1)
        assert np.all(mv2 == np.nanmax(mv.volume, axis=-1))
        assert mv2.shape == mv.shape[:3]
        mv2 = np.nanmax(mv)
        assert np.all(mv2 == np.nanmax(mv.volume))
        assert np.isscalar(mv2)

        mv2 = np.nanargmin(mv, axis=-1)
        assert np.all(mv2 == np.nanargmin(mv.volume, axis=-1))
        assert mv2.shape == mv.shape[:3]
        mv2 = np.nanargmin(mv)
        assert np.all(mv2 == np.nanargmin(mv.volume))

        mv2 = np.nanargmax(mv, axis=-1)
        assert np.all(mv2 == np.nanargmax(mv.volume, axis=-1))
        assert mv2.shape == mv.shape[:3]
        mv2 = np.nanargmax(mv)
        assert np.all(mv2 == np.nanargmax(mv.volume))

        # Round
        shape = (10, 20, 30, 2)
        affine = np.concatenate([np.random.rand(3, 4), [[0, 0, 0, 1]]], axis=0)
        mv = MedicalVolume(np.random.rand(*shape), affine, headers=headers)

        mv2 = mv.round()
        assert np.allclose(mv2.affine, affine)
        assert np.unique(mv2.volume).tolist() == [0, 1]

        mv2 = mv.round(affine=True)
        assert np.unique(mv2.affine).tolist() == [0, 1]
        assert np.unique(mv2.volume).tolist() == [0, 1]

        # Clip
        shape = (10, 20, 30)
        mv = MedicalVolume(np.random.rand(*shape), np.eye(4))

        mv2 = np.clip(mv, 0.4, 0.6)
        assert np.all((mv2.volume >= 0.4) & (mv2.volume <= 0.6))

        mv_lower = MedicalVolume(np.ones(mv.shape) * 0.4, mv.affine)
        mv_upper = MedicalVolume(np.ones(mv.shape) * 0.6, mv.affine)
        mv2 = np.clip(mv, mv_lower, mv_upper)
        assert np.all((mv2.volume >= 0.4) & (mv2.volume <= 0.6))

        # Array like
        shape = (10, 20, 30)
        mv = MedicalVolume(np.random.rand(*shape), np.eye(4))

        mv2 = np.zeros_like(mv)
        assert np.all(mv2.volume == 0)

        mv2 = np.ones_like(mv)
        assert np.all(mv2.volume == 1)

        # Shares memory
        shape = (10, 20, 30, 2)
        headers = np.stack(
            [
                ututils.build_dummy_headers(shape[2], {"EchoTime": 2}),
                ututils.build_dummy_headers(shape[2], {"EchoTime": 10}),
            ],
            axis=-1,
        )
        mv = MedicalVolume(np.random.rand(*shape), np.eye(4), headers=headers)
        mv2 = MedicalVolume(mv.A, affine=mv.affine, headers=mv.headers())
        assert np.shares_memory(mv, mv)
        assert np.shares_memory(mv, mv2)