def __init__(self, tiff_path: pathlib.Path):
        self._file_path = tiff_path
        self._metadata = ScanImageMetadata(tiff_path=tiff_path)

        self._validate_z_stack()
        self._get_z_manifest()
        self._frame_shape = dict()
示例#2
0
def test_zs_for_roi(tmp_path_factory, mock_2x3_metadata_zs,
                    mock_2x3_metadata_zsAllActuators,
                    mock_1x3_metadata_zsAllActuators,
                    mock_3x1_metadata_zsAllActuators):
    """
    Test behavior of ScanImageMetadata.zs_for_roi
    """
    tmpdir = tmp_path_factory.mktemp('test_all_zs')
    tmp_path = pathlib.Path(tempfile.mkstemp(dir=tmpdir, suffix='.tiff')[1])

    to_replace = 'ophys_etl.modules.mesoscope_splitting.'
    to_replace += 'tiff_metadata._read_metadata'

    for metadata_fixture in (mock_2x3_metadata_zs,
                             mock_2x3_metadata_zsAllActuators,
                             mock_1x3_metadata_zsAllActuators,
                             mock_3x1_metadata_zsAllActuators):

        expected_rois = metadata_fixture[2]

        with patch(to_replace, new=Mock(return_value=metadata_fixture[0])):
            metadata = ScanImageMetadata(tiff_path=tmp_path)
        assert metadata.n_rois == len(expected_rois)
        for i_roi in range(metadata.n_rois):
            assert metadata.zs_for_roi(i_roi) == expected_rois[i_roi]['zs']

        with pytest.raises(ValueError, match="You asked for ROI"):
            metadata.zs_for_roi(metadata.n_rois)
示例#3
0
def test_all_zs(tmp_path_factory, mock_2x3_metadata_zs,
                mock_2x3_metadata_zsAllActuators,
                mock_1x3_metadata_zsAllActuators,
                mock_3x1_metadata_zsAllActuators):
    """
    Test that ScanImageMetadata.all_zs() returns the expected result
    """

    tmpdir = tmp_path_factory.mktemp('test_all_zs')
    tmp_path = pathlib.Path(tempfile.mkstemp(dir=tmpdir, suffix='.tiff')[1])

    expected = [[1, 4, 2], [9, 5, 11], [2, 6, 1]]

    to_replace = 'ophys_etl.modules.mesoscope_splitting.'
    to_replace += 'tiff_metadata._read_metadata'

    for metadata_fixture in (mock_2x3_metadata_zs,
                             mock_2x3_metadata_zsAllActuators,
                             mock_1x3_metadata_zsAllActuators,
                             mock_3x1_metadata_zsAllActuators):

        expected = metadata_fixture[1]

        with patch(to_replace, new=Mock(return_value=metadata_fixture[0])):
            metadata = ScanImageMetadata(tiff_path=tmp_path)
            assert expected == metadata.all_zs()
示例#4
0
def test_all_zs_error(tmp_path_factory, mock_2x3_metadata_nozs):
    """
    Test that expected exception is thrown when metadata lacks both
    SI.hStackManager.zs and SI.hStackManager.zsAllActuators
    """

    tmpdir = tmp_path_factory.mktemp('test_all_zs')
    tmp_path = pathlib.Path(tempfile.mkstemp(dir=tmpdir, suffix='.tiff')[1])

    to_replace = 'ophys_etl.modules.mesoscope_splitting.'
    to_replace += 'tiff_metadata._read_metadata'

    with patch(to_replace, new=Mock(return_value=mock_2x3_metadata_nozs)):
        metadata = ScanImageMetadata(tiff_path=tmp_path)
    with pytest.raises(ValueError, match="Cannot load all_zs"):
        metadata.all_zs()
示例#5
0
def test_no_file_error():
    """
    Test that, ScanImageMetadata raises an error if you
    give it a path that doesn't point to a file
    """
    dummy_path = pathlib.Path('not_a_file.tiff')
    with pytest.raises(ValueError, match="is not a file"):
        ScanImageMetadata(tiff_path=dummy_path)
class ScanImageTiffSplitter(IntFromZMapperMixin):
    """
    A class to naively split up a tiff file by just looping over
    the scanfields in its ROIs

    **this will not work for z-stacks**

    Parameters
    ----------
    tiff_path: pathlib.Path
        Path to the TIFF file whose metadata we are parsing
    """
    def __init__(self, tiff_path: pathlib.Path):
        self._file_path = tiff_path
        self._metadata = ScanImageMetadata(tiff_path=tiff_path)

        self._validate_z_stack()
        self._get_z_manifest()
        self._frame_shape = dict()

    def _validate_z_stack(self):
        """
        Make sure that the zsAllActuators are arranged the
        way we expect, i.e.
        [[roi0_z0, roi0_z1, roi0_z2..., roi0_zM],
         [roi0_zM+1, roi0_zM+2, ...],
         [roi1_z0, roi1_z1, roi1_z2..., roi1_zM],
         [roi1_zM+1, roi1_zM+2, ...],
         ...
         [roiN_z0, roiN_z1, roiN_z2...]]

        or, in the case of one ROI

        [z0, z1, z2....]
        """
        z_value_array = self._metadata.all_zs()

        # check that z_value_array is a list of lists
        if not isinstance(z_value_array, list):
            msg = "Unclear how to split this TIFF\n"
            msg += f"{self._file_path.resolve().absolute()}\n"
            msg += f"metadata.all_zs {self._metadata.all_zs()}"
            raise RuntimeError(msg)

        if isinstance(z_value_array[0], list):
            z_value_array = np.concatenate(z_value_array)
        defined_rois = self._metadata.defined_rois

        z_int_per_roi = []

        msg = ""

        # check that the same Z value does not appear more than
        # once in the same ROI
        for i_roi, roi in enumerate(defined_rois):
            if isinstance(roi['zs'], list):
                roi_zs = roi['zs']
            else:
                roi_zs = [
                    roi['zs'],
                ]
            roi_z_ints = [self._int_from_z(z_value=zz) for zz in roi_zs]
            z_int_set = set(roi_z_ints)
            if len(z_int_set) != len(roi_zs):
                msg += f"roi {i_roi} has duplicate zs: {roi['zs']}\n"
            z_int_per_roi.append(z_int_set)

        # check that z values in z_array occurr in ROI order
        offset = 0
        n_roi = len(z_int_per_roi)
        n_z_per_roi = len(z_value_array) // n_roi

        # check that every ROI has the same number of zs
        if len(z_value_array) % n_roi != 0:
            msg += "There do not appear to be an "
            msg += "equal number of zs per ROI\n"
            msg += f"n_z: {len(z_value_array)} "
            msg += f"n_roi: {len(z_int_per_roi)}\n"

        for roi_z_ints in z_int_per_roi:
            these_z_ints = set([
                self._int_from_z(z_value=zz)
                for zz in z_value_array[offset:offset + n_z_per_roi]
            ])
            if these_z_ints != roi_z_ints:
                these_z_ints = set([
                    self._int_from_z(z_value=zz)
                    for zz in z_value_array[offset:offset + n_z_per_roi - 1]
                ])

                # might be placeholder value == 0
                odd_value = z_value_array[offset + n_z_per_roi - 1]

                if np.abs(odd_value) >= 1.0e-6 or these_z_ints != roi_z_ints:
                    msg += "z_values from sub array "
                    msg += "not in correct order for ROIs; "
                    break
            offset += n_z_per_roi

        if len(msg) > 0:
            full_msg = "Unclear how to split this TIFF\n"
            full_msg += f"{self._file_path.resolve().absolute()}\n"
            full_msg += f"{msg}"
            full_msg += f"all_zs {self._metadata.all_zs()}\nfrom rois:\n"
            for roi in self._metadata.defined_rois:
                full_msg += f"zs: {roi['zs']}\n"
            raise RuntimeError(full_msg)

    def _get_z_manifest(self):
        """
        Populate various member objects that help us keep
        track of what z values go with what ROIs in this TIFF
        """
        local_z_value_list = np.array(self._metadata.all_zs()).flatten()
        defined_rois = self._metadata.defined_rois

        # create a list of sets indicating which z values were actually
        # scanned in the ROI (this will help us parse the placeholder
        # zeros that sometimes get dropped into
        # SI.hStackManager.zsAllActuators

        valid_z_int_per_roi = []
        valid_z_per_roi = []
        for roi in defined_rois:
            this_z_value = roi['zs']
            if isinstance(this_z_value, int):
                this_z_value = [
                    this_z_value,
                ]
            z_as_int = [self._int_from_z(z_value=zz) for zz in this_z_value]
            valid_z_int_per_roi.append(set(z_as_int))
            valid_z_per_roi.append(this_z_value)

        self._valid_z_int_per_roi = valid_z_int_per_roi
        self._valid_z_per_roi = valid_z_per_roi
        self._n_valid_zs = 0
        self._roi_z_int_manifest = []
        ct = 0
        i_roi = 0
        local_z_index_list = [
            self._int_from_z(zz) for zz in local_z_value_list
        ]
        for zz in local_z_index_list:
            if i_roi >= len(valid_z_int_per_roi):
                break
            if zz in valid_z_int_per_roi[i_roi]:
                roi_z = (i_roi, zz)
                self._roi_z_int_manifest.append(roi_z)
                self._n_valid_zs += 1
                ct += 1
                if ct == len(valid_z_int_per_roi[i_roi]):
                    i_roi += 1
                    ct = 0

    @property
    def input_path(self) -> pathlib.Path:
        """
        The file this splitter is trying to split
        """
        return self._file_path

    def is_z_valid_for_roi(self, i_roi: int, z_value: float) -> bool:
        """
        Is specified z-value valid for the specified ROI
        """
        z_as_int = self._int_from_z(z_value=z_value)
        return z_as_int in self._valid_z_int_per_roi[i_roi]

    @property
    def roi_z_int_manifest(self) -> List[Tuple[int, int]]:
        """
        A list of tuples. Each tuple is a valid
        (roi_index, z_as_int) pair.
        """
        return self._roi_z_int_manifest

    @property
    def n_valid_zs(self) -> int:
        """
        The total number of valid z values associated with this TIFF.
        """
        return self._n_valid_zs

    @property
    def n_rois(self) -> int:
        """
        The number of ROIs in this TIFF
        """
        return self._metadata.n_rois

    @property
    def n_pages(self):
        """
        The number of pages in this TIFF
        """
        if not hasattr(self, '_n_pages'):
            with tifffile.TiffFile(self._file_path, mode='rb') as tiff_file:
                self._n_pages = len(tiff_file.pages)
        return self._n_pages

    def roi_center(self, i_roi: int) -> Tuple[float, float]:
        """
        The (X, Y) center coordinates of roi_index=i_roi
        """
        return self._metadata.roi_center(i_roi=i_roi)

    def _get_offset(self, i_roi: int, z_value: float) -> int:
        """
        Get the first page associated with the specified
        i_roi, z_value pair.
        """
        found_it = False
        n_step_over = 0
        this_roi_z = (i_roi, self._int_from_z(z_value=z_value))
        for roi_z_pair in self.roi_z_int_manifest:
            if roi_z_pair == this_roi_z:
                found_it = True
                break
            n_step_over += 1
        if not found_it:
            msg = f"Could not find stride for {i_roi}, {z_value}\n"
            msg += f"TIFF file {self._file_path.resolve().absolute()}"
            raise ValueError(msg)
        return n_step_over

    def _get_pages(self, i_roi: int, z_value: float) -> List[np.ndarray]:
        """
        Get a list of np.ndarrays representing the pages of image data
        for ROI i_roi at the specified z_value
        """

        if i_roi >= self.n_rois:
            msg = f"You asked for ROI {i_roi}; "
            msg += f"there are only {self.n_rois} ROIs "
            msg += f"in {self._file_path.resolve().absolute()}"
            raise ValueError(msg)

        if not self.is_z_valid_for_roi(i_roi=i_roi, z_value=z_value):
            msg = f"{z_value} is not a valid z value for ROI {i_roi};"
            msg += f"valid z values are {self._valid_z_per_roi[i_roi]}\n"
            msg += f"TIFF file {self._file_path.resolve().absolute()}"
            raise ValueError(msg)

        offset = self._get_offset(i_roi=i_roi, z_value=z_value)

        tiff_data = []
        with tifffile.TiffFile(self._file_path, mode='rb') as tiff_file:
            for i_page in range(offset, self.n_pages, self.n_valid_zs):
                arr = tiff_file.pages[i_page].asarray()
                tiff_data.append(arr)
                key_pair = (i_roi, z_value)
                if key_pair in self._frame_shape:
                    if arr.shape != self._frame_shape[key_pair]:
                        msg = f"ROI {i_roi} z_value {z_value}\n"
                        msg += "yields inconsistent frame shape"
                        raise RuntimeError(msg)
                else:
                    self._frame_shape[key_pair] = arr.shape

        return tiff_data

    def frame_shape(self, i_roi: int,
                    z_value: Optional[float]) -> Tuple[int, int]:
        """
        Get the shape of the image for a specified ROI at a specified
        z value

        Parameters
        ----------
        i_roi: int
            index of the ROI

        z_value: Optional[float]
            value of z. If None, z_value will be detected automaticall
            (assuming there is no ambiguity)

        Returns
        -------
        frame_shape: Tuple[int, int]
            (nrows, ncolumns)
        """
        if z_value is None:
            z_value = self._get_z_value(i_roi=i_roi)

        key_pair = (i_roi, self._int_from_z(z_value))

        if key_pair not in self._frame_shape:
            offset = self._get_offset(i_roi=i_roi, z_value=z_value)
            with tifffile.TiffFile(self._file_path, mode='rb') as tiff_file:
                page = tiff_file.pages[offset].asarray()
                self._frame_shape[key_pair] = page.shape
        return self._frame_shape[key_pair]

    def _get_z_value(self, i_roi: int) -> float:
        """
        Return the z_value associated with i_roi, assuming
        there is only one. Raises a RuntimeError if there
        is more than one.
        """
        # When splitting surface TIFFs, there's no sensible
        # way to know the z-value ahead of time (whatever the
        # operator enters is just a placeholder). The block
        # of code below will scan for z-values than align with
        # the specified ROI ID and select the correct z value
        # (assuming there is only one)
        possible_z_values = []
        for pair in self.roi_z_int_manifest:
            if pair[0] == i_roi:
                possible_z_values.append(pair[1])
        if len(possible_z_values) > 1:
            msg = f"{len(possible_z_values)} possible z values "
            msg += f"for ROI {i_roi}; must specify one of\n"
            msg += f"{possible_z_values}"
            raise RuntimeError(msg)
        z_value = possible_z_values[0]
        return self._z_from_int(ii=z_value)

    def write_output_file(self, i_roi: int, z_value: Optional[float],
                          output_path: pathlib.Path) -> None:
        """
        Write the image created by averaging all of the TIFF
        pages associated with an (i_roi, z_value) pair to a TIFF
        file.

        Parameters
        ----------
        i_roi: int

        z_value: Optional[int]
            If None, will be detected automatically (assuming there
            is only one)

        output_path: pathlib.Path
            Path to file to be written

        Returns
        -------
        None
            Output is written to output_path
        """

        if output_path.suffix not in ('.tif', '.tiff'):
            msg = "expected .tiff output path; "
            msg += f"you specified {output_path.resolve().absolute()}"

        if z_value is None:
            z_value = self._get_z_value(i_roi=i_roi)
        data = np.array(self._get_pages(i_roi=i_roi, z_value=z_value))
        avg_img = np.mean(data, axis=0)
        avg_img = normalize_array(array=avg_img,
                                  lower_cutoff=None,
                                  upper_cutoff=None)
        tifffile.imsave(output_path, avg_img)
        return None
示例#7
0
    def run(self):
        t0 = time.time()
        output = {"column_stacks": []}
        files_to_record = []

        ts_path = pathlib.Path(self.args['timeseries_tif'])
        timeseries_splitter = TimeSeriesSplitter(tiff_path=ts_path)
        files_to_record.append(ts_path)

        depth_path = pathlib.Path(self.args["depths_tif"])
        depth_splitter = ScanImageTiffSplitter(tiff_path=depth_path)
        files_to_record.append(depth_path)

        surface_path = pathlib.Path(self.args["surface_tif"])
        surface_splitter = ScanImageTiffSplitter(tiff_path=surface_path)
        files_to_record.append(surface_path)

        zstack_path_list = []
        for plane_grp in self.args['plane_groups']:
            zstack_path = pathlib.Path(plane_grp['local_z_stack_tif'])
            zstack_path_list.append(zstack_path)
            files_to_record.append(zstack_path)

        zstack_splitter = ZStackSplitter(tiff_path_list=zstack_path_list)

        ready_to_archive = set()

        # Looking at recent examples of outputs from this queue,
        # I do not think we have honored the 'column_z_stack_tif'
        # entry in the schema for some time now. I find no examples
        # in which this entry of the input.jon is ever populated.
        # I am leaving it here for now to avoid the complication of
        # having to modify the ruby strategy associated with this
        # queue, which is out of scope for the work we have
        # currently committed to.
        for plane_group in self.args["plane_groups"]:
            if "column_z_stack_tif" in plane_group:
                msg = "'column_z_stack_tif' detected in 'plane_groups'; "
                msg += "the TIFF splitting code no longer handles that file."
                self.logger.warn(msg)

        # There are cases where the centers for ROIs are not
        # exact across modalities, so we cannot demand that the
        # ROI centers be the same to within an absolute tolerance.
        # Here we use the timeseries TIFF to assemble a list of all
        # available ROI centers. When splitting the other TIFFs, we
        # will validate them by making sure that the closest
        # valid_roi_center is always what we expect.
        valid_roi_centers = get_valid_roi_centers(
            timeseries_splitter=timeseries_splitter)

        experiment_metadata = []
        for plane_group in self.args["plane_groups"]:
            for experiment in plane_group["ophys_experiments"]:
                this_exp_metadata = dict()
                exp_id = experiment["experiment_id"]
                this_exp_metadata["experiment_id"] = exp_id
                for file_key in ('timeseries', 'depth_2p', 'surface_2p',
                                 'local_z_stack'):
                    this_metadata = dict()
                    for data_key in ('offset_x', 'offset_y', 'rotation',
                                     'resolution'):
                        this_metadata[data_key] = experiment[data_key]
                    this_exp_metadata[file_key] = this_metadata

                experiment_dir = pathlib.Path(experiment["storage_directory"])
                experiment_id = experiment["experiment_id"]
                roi_index = experiment["roi_index"]
                scanfield_z = experiment["scanfield_z"]
                baseline_center = None

                for (splitter, z_value, output_name, metadata_tag) in zip(
                    (depth_splitter, surface_splitter, zstack_splitter,
                     timeseries_splitter),
                    (scanfield_z, None, scanfield_z, scanfield_z),
                    (f"{experiment_id}_depth.tif",
                     f"{experiment_id}_surface.tif",
                     f"{experiment_id}_z_stack_local.h5",
                     f"{experiment_id}.h5"),
                    ("depth_2p", "surface_2p", "local_z_stack", "timeseries")):

                    output_path = experiment_dir / output_name

                    roi_center = splitter.roi_center(i_roi=roi_index)
                    nearest_valid = get_nearest_roi_center(
                        this_roi_center=roi_center,
                        valid_roi_centers=valid_roi_centers)
                    if baseline_center is None:
                        baseline_center = nearest_valid

                    if nearest_valid != baseline_center:
                        msg = f"experiment {experiment_id}\n"
                        msg += "roi center inconsistent for "
                        msg += "input: "
                        msg += f"{splitter.input_path.resolve().absolute()}\n"
                        msg += "output: "
                        msg += f"{output_path.resolve().absolute()}\n"
                        msg += f"{baseline_center}; {roi_center}\n"
                        raise RuntimeError(msg)

                    splitter.write_output_file(i_roi=roi_index,
                                               z_value=z_value,
                                               output_path=output_path)
                    str_path = str(output_path.resolve().absolute())
                    this_exp_metadata[metadata_tag]['filename'] = str_path
                    frame_shape = splitter.frame_shape(i_roi=roi_index,
                                                       z_value=z_value)
                    this_exp_metadata[metadata_tag]['height'] = frame_shape[0]
                    this_exp_metadata[metadata_tag]['width'] = frame_shape[1]

                    self.logger.info("wrote "
                                     f"{output_path.resolve().absolute()}")

                experiment_metadata.append(this_exp_metadata)

        output["experiment_output"] = experiment_metadata

        ready_to_archive.add(self.args["surface_tif"])
        ready_to_archive.add(self.args["depths_tif"])
        ready_to_archive.add(self.args["timeseries_tif"])
        for zstack_path in zstack_path_list:
            ready_to_archive.add(str(zstack_path.resolve().absolute()))

        output["ready_to_archive"] = list(ready_to_archive)

        # record file metadata
        file_metadata = []
        for file_path in files_to_record:
            tiff_metadata = ScanImageMetadata(file_path)
            this_metadata = dict()
            this_metadata['input_tif'] = str(file_path.resolve().absolute())
            this_metadata['scanimage_metadata'] = tiff_metadata._metadata[0]
            this_metadata['roi_metadata'] = tiff_metadata._metadata[1]
            file_metadata.append(this_metadata)
        output["file_metadata"] = file_metadata

        self.output(get_sanitized_json_data(output), indent=1)
        duration = time.time() - t0
        self.logger.info(f"that took {duration:.2e} seconds")
示例#8
0
    def __init__(self, tiff_path_list: List[pathlib.Path]):

        self._path_to_metadata = dict()
        self._frame_shape = dict()
        for tiff_path in tiff_path_list:
            str_path = str(tiff_path.resolve().absolute())
            self._path_to_metadata[str_path] = ScanImageMetadata(
                tiff_path=tiff_path)

        # construct lookup tables to help us map ROI index and z-value
        # to a tiff path and an index in the z-array

        # map (i_roi, z_value) pairs to TIFF file paths
        self._roi_z_int_to_path = dict()

        # map (tiff_file_path, z_value) to the index, i.e.
        # to which scanned z-value *in this TIFF* does the
        # z-value correspond.
        self._path_z_int_to_index = dict()

        # this is an internal lookup table which we will use
        # to validate that every ROI is represented by a
        # z-stack file
        roi_to_path = dict()

        for tiff_path in self._path_to_metadata.keys():
            metadata = self._path_to_metadata[tiff_path]
            this_roi = None
            for i_roi, roi in enumerate(metadata.defined_rois):
                if roi['discretePlaneMode'] == 0:
                    if this_roi is not None:
                        raise RuntimeError("More than one ROI has "
                                           "discretePlaneMode==0 for "
                                           "{tiff_path}")
                    this_roi = i_roi

            if this_roi is None:
                raise RuntimeError("Could not find discretePlaneMode==0 for "
                                   f"{tiff_path}")

            if this_roi not in roi_to_path:
                roi_to_path[this_roi] = []
            roi_to_path[this_roi].append(tiff_path)

            z_array = np.array(metadata.all_zs())
            if z_array.shape[1] != 2:
                raise RuntimeError(f"z_array for {tiff_path} has odd shape\n"
                                   f"{z_array}")

            z_mean = z_array.mean(axis=0)
            for ii, z_value in enumerate(z_mean):
                roi_z = (this_roi, self._int_from_z(z_value=z_value))
                self._roi_z_int_to_path[roi_z] = tiff_path
                path_z = (tiff_path, self._int_from_z(z_value=z_value))
                self._path_z_int_to_index[path_z] = ii

        # check that every ROI has a z-stack file
        for tiff_path in self._path_to_metadata:
            metadata = self._path_to_metadata[tiff_path]
            n_rois = len(metadata.defined_rois)
            if len(roi_to_path) != n_rois:
                msg = (f"There are {n_rois} ROIs; however, only "
                       f"{len(roi_to_path)} of them are represented in the "
                       "local z-stack TIFFS. Here is a mapping from i_roi to "
                       "TIFF paths\n"
                       f"{json.dumps(roi_to_path, indent=2, sort_keys=True)}"
                       "\n\nThis was determined by scanning the z-stack TIFFs "
                       "and noting which ROIs were marked with "
                       "discretePlaneMode==0")
                raise RuntimeError(msg)

        self._path_to_pages = dict()
        for tiff_path in self._path_to_metadata.keys():
            with tifffile.TiffFile(tiff_path, mode='rb') as tiff_file:
                self._path_to_pages[tiff_path] = len(tiff_file.pages)