Beispiel #1
0
def calibrate(ims_import_res,
              n_best_fields=6,
              divs=5,
              metadata=None,
              progress=None):
    if metadata is None:
        metadata = {}
    calib = Calibration({f"metadata.instrument": metadata})

    qdf = ims_import_res.qualities()
    quality = qdf.sort_values(["field_i", "channel_i",
                               "cycle_i"])["quality"].values.reshape(
                                   (ims_import_res.n_fields,
                                    ims_import_res.n_channels,
                                    ims_import_res.n_cycles))
    best_field_iz = np.argsort(np.sum(
        quality, axis=(1, 2)))[::-1][0:n_best_fields].tolist()

    n_cycles = ims_import_res.n_cycles
    zstack_depths = [
        0
    ] * n_cycles  # TASK: This will need to come from ims_import_res metadata
    calib.add({f"zstack_depths.instrument": zstack_depths})

    _calibrate(ims_import_res.ims[best_field_iz, :, :],
               calib,
               divs=divs,
               progress=progress)

    return calib
Beispiel #2
0
    def validate(self):
        # Note: does not call super because the override_nones is set to false here
        self.schema.apply_defaults(self.defaults,
                                   apply_to=self,
                                   override_nones=False)
        self.schema.validate(self, context=self.__class__.__name__)

        self.calibration = Calibration(self.calibration)
        if self.instrument_subject_id is not None:
            self.calibration.filter_subject_ids(self.instrument_subject_id)
            if len(self.calibration.keys()) == 0:
                raise ValueError(
                    f"All calibration records removed after filter_subject_ids on subject_id '{self.instrument_subject_id}'"
                )

        assert not self.calibration.has_subject_ids()

        if self.radiometry_channels is not None:
            pat = re.compile(r"[0-9a-z_]+")
            for name, channel_i in self.radiometry_channels.items():
                self._validate(
                    pat.fullmatch(name),
                    "radiometry_channels name must be lower-case alphanumeric (including underscore)",
                )
                self._validate(isinstance(channel_i, int),
                               "channel_i must be an integer")
Beispiel #3
0
 def it_filters():
     c = Calibration({
         "p_failure_to_bind_amino_acid.label[C].sn1234": 0.5,
         "p_failure_to_bind_amino_acid.label[C].sn0": 1.0,
     })
     c.filter_subject_ids({"sn1234"})
     assert len(c) == 1
     assert c["p_failure_to_bind_amino_acid.label[C]"] == 0.5
Beispiel #4
0
 def it_sets_subject_id():
     c = Calibration({
         "p_failure_to_bind_amino_acid.label[C]": 1.0,
         "p_failure_to_attach_to_dye.label[C]": 2.0,
     })
     c.set_subject_id("test")
     assert c["p_failure_to_bind_amino_acid.label[C].test"] == 1.0
     assert c["p_failure_to_attach_to_dye.label[C].test"] == 2.0
 def it_balances_regionally():
     sigproc_params = SigprocV2Params(
         radiometry_channels=dict(aaa=0, bbb=1),
         calibration=Calibration(
             {
                 "regional_illumination_balance.instrument_channel[0].test": [
                     [1.0, 5.0],
                     [1.0, 1.0],
                 ],
                 "regional_illumination_balance.instrument_channel[1].test": [
                     [7.0, 1.0],
                     [1.0, 1.0],
                 ],
                 "regional_bg_mean.instrument_channel[0].test": [
                     [100.0, 100.0],
                     [100.0, 100.0],
                 ],
                 "regional_bg_mean.instrument_channel[1].test": [
                     [200.0, 200.0],
                     [200.0, 200.0],
                 ],
             }
         ),
         instrument_subject_id="test",
     )
     chcy_ims = np.ones((2, 1, 128, 128))
     chcy_ims[0] *= 1000.0
     chcy_ims[1] *= 2000.0
     balanced_ims = worker._import_balanced_images(chcy_ims, sigproc_params)
     assert np.all(np.isclose(balanced_ims[0, 0, 0, 0], (1000 - 100) * 2))
     assert np.all(np.isclose(balanced_ims[0, 0, 0, 127], (1000 - 100) * 2 * 5))
     assert np.all(np.isclose(balanced_ims[1, 0, 0, 0], (2000 - 200) * 1 * 7))
     assert np.all(np.isclose(balanced_ims[1, 0, 127, 0], (2000 - 200) * 1))
    def generate(self):
        run_descs = []

        calibration = Calibration.from_yaml(self.calibration_file)

        sigproc_tasks = self.sigprocs_v2(
            calibration=calibration, instrument_subject_id=self.instrument_subject_id,
        )
        if len(sigproc_tasks) == 0:
            raise ValueError(
                "No sigprocv2 tasks were found. This might be due to an empty block of another switch."
            )

        for sigproc_i, sigproc_task in enumerate(sigproc_tasks):
            lnfit_tasks = self.lnfits()
            sigproc_source = ""
            for k, v in sigproc_task.items():
                if "ims_import" in k:
                    sigproc_source = local.path(v.inputs.src_dir).name
                    break

            run_name = f"sigproc_v2_{sigproc_i}_{sigproc_source}"
            if self.force_run_name is not None:
                run_name = self.force_run_name

            run_desc = Munch(run_name=run_name, **sigproc_task, **lnfit_tasks,)

            sigproc_template = "sigproc_v2_template.ipynb"
            if self.movie:
                sigproc_template = "sigproc_v2_movie_template.ipynb"

            self.report_section_markdown(f"# RUN {run_desc.run_name}\n")
            self.report_section_run_object(run_desc)

            self.report_section_from_template(sigproc_template)
            if lnfit_tasks:
                self.report_section_from_template("lnfit_template.ipynb")

            run_descs += [run_desc]

        n_run_descs = len(run_descs)
        self.report_preamble(
            utils.smart_wrap(
                f"""
                # Signal Processing Overview
                ## {n_run_descs} run(s) processed.
            """
            )
        )

        return run_descs
Beispiel #7
0
def sigproc(sigproc_params, ims_import_result, progress=None):
    """
    Analyze all fields
    """
    calib = Calibration(sigproc_params.calibration)
    assert not calib.is_empty()

    channel_weights = _compute_channel_weights(sigproc_params)

    sigproc_result = SigprocV2Result(
        params=sigproc_params,
        n_input_channels=ims_import_result.n_channels,
        n_channels=sigproc_params.n_output_channels,
        n_cycles=ims_import_result.n_cycles,
        channel_weights=channel_weights,
    )

    n_fields = ims_import_result.n_fields
    n_fields_limit = sigproc_params.n_fields_limit
    if n_fields_limit is not None and n_fields_limit < n_fields:
        n_fields = n_fields_limit

    zap.work_orders(
        [
            Munch(
                fn=_do_sigproc_field,
                ims_import_result=ims_import_result,
                sigproc_params=sigproc_params,
                field_i=field_i,
                sigproc_result=sigproc_result,
            ) for field_i in range(n_fields)
        ],
        _trap_exceptions=False,
        _progress=progress,
    )

    return sigproc_result
    def _setup(corner_bal):
        divs = 5
        bg = 100 * np.ones((divs, divs))
        bal = np.ones((divs, divs))
        bal[0, 0] = corner_bal

        chcy_ims = 101 * np.ones((2, 4, 512, 512))
        chcy_ims[1, :, :, :] += 1

        calib = Calibration(
            {
                "regional_bg_mean.instrument_channel[0]": bg.tolist(),
                "regional_illumination_balance.instrument_channel[0]": bal.tolist(),
                "regional_bg_mean.instrument_channel[1]": bg.tolist(),
                "regional_illumination_balance.instrument_channel[1]": bal.tolist(),
            }
        )

        return chcy_ims, calib
    def it_returns_balanced_channels():
        sigproc_params = SigprocV2Params(
            radiometry_channels=dict(aaa=0, bbb=1),
            calibration=Calibration(
                {
                    "regional_bg_mean.instrument_channel[0].test": [
                        [100.0, 100.0],
                        [100.0, 100.0],
                    ],
                    "regional_bg_mean.instrument_channel[1].test": [
                        [200.0, 200.0],
                        [200.0, 200.0],
                    ],
                }
            ),
            instrument_subject_id="test",
        )

        balance = worker._compute_channel_weights(sigproc_params)
        assert np.all(balance == [2.0, 1.0])
Beispiel #10
0
 def it_checks_variable_subtype():
     with zest.raises(TypeError):
         Calibration(
             {"p_failure_to_bind_amino_acid.label[foo].sn1234": 1.0})
Beispiel #11
0
def _calibrate(flchcy_ims, divs=5, progress=None, overload_psf=None):
    """
    Accumulate calibration data using a set of fields.

    Arguments:
        flchcy_ims: frame, channel, cycles ims to be analyzed
            These are typically only a small subset of high quality fields.
            NOTE: "Cycles" here are considered to be z-stack slices, NOT chem-cycles.
        divs: The regional sub-divisions.
    """

    n_fields, n_channels, n_cycles = flchcy_ims.shape[0:3]
    n_z_slices = n_cycles  # This is just an alias to remind me that cycle=z-slice here.

    peak_mea = 11
    peak_dim = (peak_mea, peak_mea)

    if overload_psf is not None:
        # This is used for testing
        peak_dim = overload_psf.shape

    calib = Calibration()

    for ch_i in range(n_channels):
        z_and_region_to_psf = np.zeros((n_z_slices, divs, divs, *peak_dim))

        # BACKGROUND
        # Masks out the foreground and uses remaining pixels to estimate
        # regional background mean and std.
        # --------------------------------------------------------------

        flcy_calibs = [
            _calibrate_bg_and_psf_im(flchcy_ims[fl_i, ch_i, cy_i])
            for fl_i in range(n_fields) for cy_i in range(n_cycles)
        ]

        calib.add({
            f"regional_bg_mean.instrument_channel[{ch_i}]":
            np.mean([
                np.array(
                    flcy_calibs[f"regional_bg_mean.instrument_channel[{ch_i}]"]
                ) for c in flcy_calibs
            ])
        })

        # reg_psfs = np.sum([
        #     np.array(c[f"regional_psf_zstack.instrument_channel[{ch_i}]"])
        #     for c in flcy_calibs
        # ], axis=(2, 3))
        #
        # denominator = np.sum(z_and_region_to_psf, axis=(2, 3))[:, :, None, None]
        # calib.add({
        #     f"regional_psf_zstack.instrument_channel[{ch_i}]": reg_psfs /
        # })
        #
        # z_and_region_to_psf = utils.np_safe_divide(z_and_region_to_psf, denominator)
        #
        # calib.add({
        #     f"regional_bg_std.instrument_channel[{ch_i}]": np.mean([
        #         np.array(c[f"regional_bg_std.instrument_channel[{ch_i}]"])
        #         for c in flcy_calibs
        #     ])
        # })

        # if overload_psf is not None:
        #     # This is used for testing
        #     z_and_region_to_psf = np.broadcast_to(
        #         overload_psf, (n_z_slices, divs, divs, *peak_dim)
        #     ).tolist()
        #
        # else:
        #     # PSF
        #     # Accumulate the PSF regionally over every field
        #     # Then divide each PSF though by it's own mass so that the
        #     # AUC under each PSF is 1.
        #     # --------------------------------------------------------------
        #     [
        #         _calibrate_bg_im(flchcy_ims[fl_i, ch_i, cy_i], regional_bg_mean, regional_bg_std)
        #         for fl_i in range(n_fields)
        #         for cy_i in range(n_cycles)
        #     ]
        #
        #     for fl_i in range(n_fields):
        #
        #         for cy_i in range(n_cycles):
        #             # Remember: cy_i is a pseudo-cycle: it is really a z-slice
        #             # with z_depths[cy_i] holding the actual depth
        #
        #             regional_bg_mean = np.array(
        #                 calib[f"regional_bg_mean.instrument_channel[{ch_i}]"]
        #             )
        #             _calibrate_psf_im(flchcy_ims[fl_i, ch_i, cy_i], regional_bg_mean)
        #
        #             # ACCUMULATE each field, will normalize at the end
        #             z_and_region_to_psf[cy_i] += reg_psfs

        # # NORMALIZE all psfs
        # denominator = np.sum(z_and_region_to_psf, axis=(3, 4))[:, :, :, None, None]
        # z_and_region_to_psf = utils.np_safe_divide(z_and_region_to_psf, denominator)
        #
        # calib.add(
        #     {
        #         f"regional_psf_zstack.instrument_channel[{ch_i}]": z_and_region_to_psf.tolist()
        #     }
        # )

        # FOREGROUND
        # Runs the standard sigproc_field analysis (without balancing)
        # to get the regional radmats for regional histogram balancing.
        # This requires that the PSF already be estimated so that the
        # radiometry can run.
        # --------------------------------------------------------------

        # Spoof the sigproc_v2 worker into bypassing illumination balance by giving it all zeros
        calib.add({
            f"regional_illumination_balance.instrument_channel[{ch_i}]":
            np.ones((divs, divs)).tolist()
        })

        sigproc_params = SigprocV2Params(
            calibration=calib,
            instrument_subject_id=None,
            radiometry_channels=dict(ch=ch_i),
        )
        fl_radmats = []
        fl_locs = []
        for fl_i in range(n_fields):
            if progress is not None:
                progress(fl_i, n_fields)
            chcy_ims = flchcy_ims[fl_i, ch_i:(ch_i + 1), :]
            (
                chcy_ims,
                locs,
                radmat,
                aln_offsets,
                aln_scores,
            ) = sigproc_field(chcy_ims, sigproc_params)
            fl_radmats += [radmat]
            fl_locs += [locs]
        fl_radmat = np.concatenate(fl_radmats)
        fl_loc = np.concatenate(fl_locs)

        # BALANCE
        sig = np.nan_to_num(fl_radmat[:, ch_i, :, 0].flatten())
        noi = fl_radmat[:, ch_i, :, 1].flatten()
        snr = np.nan_to_num(sig / noi)

        locs = np.tile(fl_loc, (1, n_cycles)).reshape((-1, 2))

        snr_mask = snr > 10
        sig = sig[snr_mask]
        locs = locs[snr_mask]

        top = np.max((locs[:, 0], locs[:, 1]))
        y = utils.ispace(0, top, divs + 1)
        x = utils.ispace(0, top, divs + 1)

        def regional_locs_mask(yi, xi):
            """Create a mask for locs inside of a region"""
            mask = (y[yi] <= locs[:, 0]) & (locs[:, 0] < y[yi + 1])
            mask &= (x[xi] <= locs[:, 1]) & (locs[:, 1] < x[xi + 1])
            return mask

        medians = np.zeros((divs, divs))
        for yi in range(len(y) - 1):
            for xi in range(len(x) - 1):
                loc_mask = regional_locs_mask(yi, xi)
                bright_mask = sig > 2.0
                _sig = sig[loc_mask & bright_mask]
                medians[yi, xi] = np.median(_sig)

        center = np.max(medians)
        balance = np.zeros((divs, divs))
        for yi in range(len(y) - 1):
            for xi in range(len(x) - 1):
                loc_mask = regional_locs_mask(yi, xi)
                bright_mask = sig > 2.0
                _sig = sig[loc_mask & bright_mask]

        for yi in range(len(y) - 1):
            for xi in range(len(x) - 1):
                loc_mask = regional_locs_mask(yi, xi)
                bright_mask = sig > 2.0
                _sig = sig[loc_mask & bright_mask]
                median = np.median(_sig)
                balance[yi, xi] = center / median
                _sig *= balance[yi, xi]

        calib.add({
            f"regional_illumination_balance.instrument_channel[{ch_i}]":
            balance.tolist()
        })

    return calib
Beispiel #12
0
 def it_adds():
     c = Calibration(
         {"p_failure_to_bind_amino_acid.label[C].batch_2020_03_01": 1.0})
     c.add({"p_failure_to_attach_to_dye.label[C].batch_2020_03_01": 2.0})
     assert len(c) == 2
Beispiel #13
0
 def it_checks_prop():
     with zest.raises(TypeError):
         Calibration({"not_a_property.instrument.sn1234": 1})
Beispiel #14
0
 def it_checks_subject_type():
     with zest.raises(TypeError):
         Calibration(
             {"p_failure_to_bind_amino_acid.not_a_subject.sn1234": 1})
Beispiel #15
0
 def calib(self):
     return Calibration(self.params.calibration)
Beispiel #16
0
 def it_checks_value():
     with zest.raises(TypeError):
         Calibration({
             "p_failure_to_bind_amino_acid.label[C].sn1234":
             "not_a_float"
         })
Beispiel #17
0
 def it_checks_propsub():
     with zest.raises(TypeError):
         Calibration(
             {"p_failure_to_bind_amino_acid.instrument.sn1234": 1})
Beispiel #18
0
 def it_checks_subject_id():
     with zest.raises(TypeError):
         Calibration({"p_failure_to_bind_amino_acid.label[C].1234": 1})
Beispiel #19
0
 def it_checks_metadata():
     with zest.raises(TypeError):
         Calibration({"metadata.instrument.sn1234": "not a dict"})
Beispiel #20
0
 def it_allows_metadata():
     c = Calibration({"metadata.instrument.sn1234": dict(a=1)})
     assert c["metadata.instrument.sn1234"]["a"] == 1
Beispiel #21
0
class SigprocV2Params(Params):
    defaults = dict(
        radiometry_channels=None,
        n_fields_limit=None,
        save_full_signal_radmat_npy=False,
        # use_cycle_zero_psfs_only=False,
    )

    schema = s(
        s.is_kws_r(
            radiometry_channels=s.is_dict(noneable=True),
            n_fields_limit=s.is_int(noneable=True),
            save_full_signal_radmat_npy=s.is_bool(),
            calibration=s.is_dict(),
            instrument_subject_id=s.is_str(noneable=True),
            # use_cycle_zero_psfs_only=s.is_bool(),
        ))

    def validate(self):
        # Note: does not call super because the override_nones is set to false here
        self.schema.apply_defaults(self.defaults,
                                   apply_to=self,
                                   override_nones=False)
        self.schema.validate(self, context=self.__class__.__name__)

        self.calibration = Calibration(self.calibration)
        if self.instrument_subject_id is not None:
            self.calibration.filter_subject_ids(self.instrument_subject_id)
            if len(self.calibration.keys()) == 0:
                raise ValueError(
                    f"All calibration records removed after filter_subject_ids on subject_id '{self.instrument_subject_id}'"
                )

        assert not self.calibration.has_subject_ids()

        if self.radiometry_channels is not None:
            pat = re.compile(r"[0-9a-z_]+")
            for name, channel_i in self.radiometry_channels.items():
                self._validate(
                    pat.fullmatch(name),
                    "radiometry_channels name must be lower-case alphanumeric (including underscore)",
                )
                self._validate(isinstance(channel_i, int),
                               "channel_i must be an integer")

    def set_radiometry_channels_from_input_channels_if_needed(
            self, n_channels):
        if self.radiometry_channels is None:
            # Assume channels from nd2 manifest
            channels = list(range(n_channels))
            self.radiometry_channels = {f"ch_{ch}": ch for ch in channels}

    @property
    def n_output_channels(self):
        return len(self.radiometry_channels.keys())

    @property
    def n_input_channels(self):
        return len(self.radiometry_channels.keys())

    # @property
    # def channels_cycles_dim(self):
    #     # This is a cache set in sigproc_v1.
    #     # It is a helper for the repetitive call:
    #     # n_outchannels, n_inchannels, n_cycles, dim =
    #     return self._outchannels_inchannels_cycles_dim

    def _input_channels(self):
        """
        Return a list that converts channel number of the output to the channel of the input
        Example:
            input might have channels ["foo", "bar"]
            the radiometry_channels has: {"bar": 0}]
            Thus this function returns [1] because the 0th output channel is mapped
            to the "1" input channel
        """
        return [
            self.radiometry_channels[name]
            for name in sorted(self.radiometry_channels.keys())
        ]

    # def input_names(self):
    #     return sorted(self.radiometry_channels.keys())

    def output_channel_to_input_channel(self, out_ch):
        return self._input_channels()[out_ch]

    def input_channel_to_output_channel(self, in_ch):
        """Not every input channel necessarily has an output; can return None"""
        return utils.filt_first_arg(self._input_channels(),
                                    lambda x: x == in_ch)
Beispiel #22
0
def sigproc_field(chcy_ims, sigproc_params, snr_thresh=None):
    """
    Analyze one field and return values (do not save)

    Arguments:
        chcy_ims: In input order (from ims_import_result)
        sigproc_params: The SigprocParams
        snr_thresh: if non-None keeps only locs with S/R > snr_thresh
            This is useful for debugging.
    """

    calib = Calibration(sigproc_params.calibration)

    # Step 1: Load the images in output channel order, balance, equalize
    chcy_ims = _import_balanced_images(chcy_ims, sigproc_params)
    # At this point, chcy_ims has its background subtracted and it is
    # regionally balanced and channel equalized. It may contain negative
    # values
    #
    # NOTE: at this point, chcy_ims are in OUTPUT CHANNEL order!
    n_out_channels, n_cycles = chcy_ims.shape[0:2]
    assert n_out_channels == sigproc_params.n_output_channels

    # Step 2: Remove anomalies
    for ch_i, cy_ims in enumerate(chcy_ims):
        chcy_ims[ch_i] = imops.stack_map(cy_ims, _mask_anomalies_im)

    # Step 3: Find alignment offsets
    aln_offsets, aln_scores = _align(np.mean(chcy_ims, axis=0))

    # Step 4: Composite with alignment
    chcy_ims = _composite_with_alignment_offsets_chcy_ims(
        chcy_ims, aln_offsets)
    # chcy_ims is now only the intersection region so it may be smaller than the original

    # Step 5: Peak find on combined channels
    # The goal of previous channel equalization and regional balancing is that
    # all pixels are now on an equal footing so we can now use
    # a single values for fg_thresh and bg_thresh.
    ch_mean_of_cy0_im = np.mean(chcy_ims[:, 0, :, :], axis=0)
    locs = _peak_find(ch_mean_of_cy0_im)

    # Step 6: Radiometry over each channel, cycle
    # TASK: Eventually this will examine each cycle and decide
    # which z-depth of the PSFs is best fit to that cycle.
    # The result will be a per-cycle index into the chcy_regional_psfs
    # Until then the index is hard-coded to the zero-th index of regional_psf_zstack
    ch_z_reg_psfs = np.stack(
        [
            np.array(calib[
                f"regional_psf_zstack.instrument_channel[{sigproc_params.output_channel_to_input_channel(out_ch_i)}]"]
                     ) for out_ch_i in range(n_out_channels)
        ],
        axis=0,
    )
    assert ch_z_reg_psfs.shape[0] == n_out_channels
    cycle_to_z_index = np.zeros((n_cycles, )).astype(int)
    radmat = _radiometry(chcy_ims, locs, ch_z_reg_psfs, cycle_to_z_index)

    # Step 7: Remove empties
    # Keep any loc that has a signal > 20 times the minimum bg std in any channel
    # The 20 was found somewhat empirically and may need to be adjusted
    keep_mask = np.zeros((radmat.shape[0], )) > 0
    for out_ch_i in range(n_out_channels):
        in_ch_i = sigproc_params.output_channel_to_input_channel(out_ch_i)
        bg_std = np.min(
            calib[f"regional_bg_std.instrument_channel[{in_ch_i}]"])
        keep_mask = keep_mask | np.any(radmat[:, out_ch_i, :, 0] > 20 * bg_std,
                                       axis=1)

    if snr_thresh is not None:
        snr = radmat[:, :, :, 0] / radmat[:, :, :, 1]
        keep_mask = keep_mask & np.any(np.nan_to_num(snr) > snr_thresh,
                                       axis=(1, 2))

    return chcy_ims, locs[keep_mask], radmat[
        keep_mask], aln_offsets, aln_scores