Exemplo n.º 1
0
    def render(self, im, fl_i, ch_i, cy_i, aln_offset):
        super().render(im, fl_i, ch_i, cy_i, aln_offset)

        blob = imops.generate_circle_mask(self.size, size=self.size * 3)
        imops.accum_inplace(
            im, self.amp * blob, XY(0.25 * im.shape[0], 0.25 * im.shape[0]), center=True
        )
Exemplo n.º 2
0
 def it_adds_a_sub_image_into_a_target():
     dst = np.ones(WH(4, 4))
     src = np.ones(WH(2, 2))
     imops.accum_inplace(dst, src, XY(1, 1))
     good = np.array([[1, 1, 1, 1], [1, 2, 2, 1], [1, 2, 2, 1],
                      [1, 1, 1, 1]])
     assert (dst == good).all()
Exemplo n.º 3
0
 def render(self, im, fl_i, ch_i, cy_i, aln_offset):
     super().render(im, fl_i, ch_i, cy_i, aln_offset)
     size = int(self.std * 2.5)
     size += 1 if size % 2 == 0 else 0
     bg_mean = np.median(im) - 1
     blur = cv2.GaussianBlur(im, (size, size), self.std) - bg_mean - 1
     imops.accum_inplace(im, self.scale * blur, XY(0, 0), center=False)
Exemplo n.º 4
0
    def render(self, im, cy_i):
        if self.std_x is None:
            self.std_x = [self.std]
        if self.std_y is None:
            self.std_y = [self.std]

        n_locs = len(self.locs)
        if len(self.std_x) != n_locs:
            self.std_x = np.repeat(self.std_x, (n_locs, ))
        if len(self.std_y) != n_locs:
            self.std_y = np.repeat(self.std_y, (n_locs, ))

        super().render(im, cy_i)
        mea = 17
        for loc, amp, std_x, std_y in zip(self.locs, self.amps, self.std_x,
                                          self.std_y):
            frac_x = np.modf(loc[0])[0]
            frac_y = np.modf(loc[1])[0]
            peak_im = imops.gauss2_rho_form(
                amp=amp,
                std_x=std_x,
                std_y=std_y,
                pos_x=mea // 2 + frac_x,
                pos_y=mea // 2 + frac_y,
                rho=0.0,
                const=0.0,
                mea=mea,
            )

            imops.accum_inplace(im,
                                peak_im,
                                loc=YX(*np.floor(loc)),
                                center=True)
Exemplo n.º 5
0
 def render(self, im, cy_i):
     super().render(im, cy_i)
     for loc, amp, z_i in zip(self.locs, self.amps, self.z_iz):
         frac_part, int_part = np.modf(loc)
         shifted_peak_im = imops.sub_pixel_shift(self.z_to_psf[z_i],
                                                 frac_part)
         imops.accum_inplace(im,
                             amp * shifted_peak_im,
                             loc=YX(*int_part),
                             center=True)
Exemplo n.º 6
0
 def _peak(into_im, x, y, rho=0.0, mea=15, amp=1000.0, peak_sigma=1.8):
     corner_y = int(y - mea / 2.0 + 0.5)  # int
     corner_x = int(x - mea / 2.0 + 0.5)
     off_y = y - corner_y  # float
     off_x = x - corner_x
     peak_im = imops.gauss2_rho_form(amp, peak_sigma, peak_sigma, off_x,
                                     off_y, rho, 0.0, mea)
     imops.accum_inplace(into_im,
                         peak_im,
                         loc=XY(corner_x, corner_y),
                         center=False)
Exemplo n.º 7
0
def _step_5_radiometry(peak_df, field_df, aligned_raw_chcy_ims,
                       sigproc_params):
    """
    Radiometry measures the area under the curve of the PSF
    """

    n_outchannels, n_inchannels, n_cycles, dim = sigproc_params.channels_cycles_dim
    n_peaks = len(peak_df)

    hat_rad = sigproc_params.hat_rad
    brim_rad = sigproc_params.hat_rad + 1
    hat_mask, brim_mask = _hat_masks(hat_rad, brim_rad)

    signal_radmat = np.zeros((n_peaks, n_outchannels, n_cycles))
    noise_radmat = np.zeros((n_peaks, n_outchannels, n_cycles))
    localbg_radmat = np.zeros((n_peaks, n_outchannels, n_cycles))

    trace_dim = HW(brim_mask.shape) + HW(4, 4)
    localbg_mask = np.zeros(trace_dim)
    imops.accum_inplace(localbg_mask,
                        brim_mask,
                        loc=trace_dim / 2,
                        center=True)
    localbg_mask = localbg_mask > 0

    medians_by_chcy = (field_df.set_index(["channel_i",
                                           "cycle_i"]).sort_index().bg_median)

    # This triple-nested loop is a potential point of optimization
    # But it is probably dwarfed by the time it takes to do a center-of-mass
    # calculation for each peak on each channel/cycle.
    # Note that I was previously doing the COM calculation only
    # once for a cycle stack but since I don't do sub-pixel
    # alignment on each cycle I think it will be more accurate
    # to re-do the COM calculation on every cycle.
    for peak_i, peak_row in peak_df.iterrows():
        for outch in range(n_outchannels):
            for cycle in range(n_cycles):
                signal, noise, localbg = _radiometry(
                    aligned_raw_chcy_ims[outch, cycle],
                    XY(peak_row.aln_x, peak_row.aln_y),
                    trace_dim,
                    medians_by_chcy[outch, cycle],
                    localbg_mask,
                )
                signal_radmat[peak_i, outch, cycle] = signal
                noise_radmat[peak_i, outch, cycle] = noise
                localbg_radmat[peak_i, outch, cycle] = localbg

    # Any peak that has a NAN in it anywhere, zero it out
    _remove_nans_from_radiometry(signal_radmat, noise_radmat, localbg_radmat)

    return signal_radmat, noise_radmat, localbg_radmat
Exemplo n.º 8
0
    def render(self, im, fl_i, ch_i, cy_i, aln_offset):
        super().render(im, fl_i, ch_i, cy_i, aln_offset)

        ch_scale = 1.0
        if self._channel_scale_factor is not None:
            ch_scale = self._channel_scale_factor[ch_i]

        for loc, amp, k in zip(self.locs, self._amps, self.row_k):
            loc = loc + aln_offset

            if isinstance(amp, np.ndarray):
                amp = amp[cy_i]

            psf_im, accum_to_loc = self.reg_psf.render_at_loc(
                loc, amp=ch_scale * k * amp, const=0.0
            )
            imops.accum_inplace(im, psf_im, loc=YX(accum_to_loc), center=False)
Exemplo n.º 9
0
def spotty_images():
    # CREATE a test spot
    spot = imops.generate_gauss_kernel(2.0)
    spot = spot / np.max(spot)

    dim = WH(50, 50)
    spot_locs = [XY(15, 15), XY(10, 20), XY(20, 21)]

    # CREATE test images with spots
    test_images = []
    for loc in spot_locs:
        im = np.zeros(dim)
        # im = np.random.normal(0, 0.1, dim)
        # im = np.ones(dim) * 0.1
        imops.accum_inplace(im, spot, loc=loc, center=True)
        test_images += [im]

    return spot_locs, np.array(test_images)
Exemplo n.º 10
0
def _step_2c_composite_channels(chcy_ims, medians_by_ch_cy, sigproc_params):
    """
    Merges specified channels for each cycle after background subtraction.

    The channel list is sometimes partial because there are some
    experiments where data was recorded in a channel that is bad.

    Returns:
        composite cy_ims (channels merged) after background subtraction
    """
    n_outchannels, n_inchannels, n_cycles, dim = sigproc_params.channels_cycles_dim

    dst_cy_ims = np.zeros((n_cycles, dim, dim))
    for cy in range(n_cycles):
        for inch in range(n_inchannels):
            if inch in sigproc_params.channel_indices_for_alignment:
                imops.accum_inplace(
                    dst_cy_ims[cy],
                    (chcy_ims[inch, cy] - medians_by_ch_cy[inch, cy]).clip(min=0),
                )

    return dst_cy_ims
Exemplo n.º 11
0
 def render(self, im, fl_i, ch_i, cy_i, aln_offset):
     super().render(im, fl_i, ch_i, cy_i, aln_offset)
     bg = np.random.normal(loc=self.bg_mean, scale=self.bg_std, size=self.dim)
     imops.accum_inplace(im, bg, XY(0, 0), center=False)
Exemplo n.º 12
0
def _raw_peak_i_zoom(
        field_i,
        res,
        df,
        peak_i,
        channel=0,
        zoom=3.0,
        square_radius=7,
        x_pad=0,
        cspan=(0, 5_000),
        separate=False,
        show_circles=True,
):
    peak_i = int(peak_i)
    peak_records = df[df.peak_i == peak_i]
    field_i = int(peak_records.iloc[0].field_i)

    im = res.raw_chcy_ims(field_i)
    all_sig = res.sig()

    square = cspan[1] * imops.generate_square_mask(square_radius)

    sig_for_channel = all_sig[peak_i, channel, :]
    sig_top = np.median(sig_for_channel) + np.percentile(sig_for_channel, 99.9)

    height = (square_radius + 1) * 2 + 1
    one_width = height + x_pad
    all_width = one_width * res.n_cycles - x_pad
    im_all_cycles = np.zeros((height, all_width))

    f_plot_height = height * zoom
    f_plot_width = all_width * zoom

    z = ZPlots()
    if show_circles:
        for cycle_i in range(res.n_cycles):
            im_with_marker = np.copy(im[channel, cycle_i])
            cy_rec = peak_records[peak_records.cycle_i == cycle_i].iloc[0]
            loc = XY(cy_rec.raw_x, cy_rec.raw_y)

            imops.accum_inplace(im_with_marker, square, loc=loc, center=True)
            im_with_marker = imops.extract_with_mask(
                im_with_marker,
                imops.generate_square_mask(square_radius + 1, True),
                loc=loc,
                center=True,
            )
            imops.accum_inplace(im_all_cycles,
                                im_with_marker,
                                loc=XY(cycle_i * one_width, 0))

            if separate:
                z.im(
                    im_with_marker,
                    _noaxes=True,
                    f_plot_height=int(f_plot_height),
                    f_plot_width=int(f_plot_height),
                    _notools=True,
                    f_match_aspect=True,
                    _cspan=cspan,
                )

    if not separate:
        z.im(
            im_all_cycles,
            _noaxes=True,
            f_plot_height=int(f_plot_height),
            f_plot_width=int(f_plot_width),
            _notools=True,
            f_match_aspect=True,
            _cspan=cspan,
        )
Exemplo n.º 13
0
    def show_raw(
        peak_i,
        field_i,
        channel_i,
        cycle_i,
        min_bright=min_bright,
        max_bright=max_bright,
        show_circles=show_circles,
    ):
        field_i = int(field_i) if field_i != "" else None
        channel_i = int(channel_i)
        cycle_i = int(cycle_i)
        if field_i is None:
            peak_i = int(peak_i)
            peak_records = df[df.peak_i == peak_i]
            field_i = int(peak_records.iloc[0].field_i)
        else:
            peak_i = None

        all_sig = res.sig()

        # mask_rects_for_field = res.raw_mask_rects_df()[field_i]
        # Temporarily removed. This is going to involve some groupby Super-Pandas-Kungfu(tm)
        # Here is my too-tired start...
        """
        import pandas as pd
        df = pd.DataFrame([
            (0, 0, 0, 100, 110, 120, 130),
            (0, 0, 1, 101, 111, 121, 131),
            (0, 0, 2, 102, 112, 122, 132),
            (0, 1, 0, 200, 210, 220, 230),
            (0, 1, 1, 201, 211, 221, 231),
            (0, 1, 2, 202, 212, 222, 232),
            (1, 0, 0, 1100, 1110, 1120, 1130),
            (1, 0, 1, 1101, 1111, 1121, 1131),
            (1, 0, 2, 1102, 1112, 1122, 1132),
            (1, 1, 0, 1200, 1210, 1220, 1230),
            (1, 1, 1, 1201, 1211, 1221, 1231),
        ], columns=["frame_i", "ch_i", "cy_i", "x", "y", "w", "h"])

        def rec(row):
            return row[["x", "y"]]

        df.set_index("frame_i").groupby(["frame_i"]).apply(rec)
        """
        mask_rects_for_field = None

        cspan = (min_bright, max_bright)
        circle = cspan[1] * imops.generate_donut_mask(4, 3)
        square = cspan[1] * imops.generate_square_mask(square_radius)

        z = ZPlots()
        sig_for_channel = all_sig[:, channel_i, :]
        sig_top = np.median(sig_for_channel) + np.percentile(
            sig_for_channel, 99.9)

        if peak_i is not None:
            rad = sig_for_channel[peak_i]
            rad = rad.reshape(1, rad.shape[0])
            print("\n".join([
                f"    cycle {cycle:2d}: {r:6.0f}"
                for cycle, r in enumerate(rad[0])
            ]))
            z.scat(x=range(len(rad[0])), y=rad[0])
            z.im(rad, _cspan=(0, sig_top), f_plot_height=50, _notools=True)

            # This is inefficient because the function we will call
            # does the same image load, but I'd prefer to not repeat
            # the code here and want to be able to call this fn
            # from notebooks:
            _raw_peak_i_zoom(
                field_i,
                res,
                df,
                peak_i,
                channel_i,
                zoom=3.0,
                square_radius=square_radius,
                x_pad=1,
                cspan=cspan,
                separate=False,
                show_circles=show_circles,
            )

        if result_block == "sigproc_v1":
            im = res.raw_chcy_ims(field_i).copy()[channel_i, cycle_i]
        else:
            im = res.aln_ims[field_i, channel_i, cycle_i].copy()

        if peak_i is not None:
            cy_rec = peak_records[peak_records.cycle_i == cycle_i].iloc[0]
            im_marker = square if peak_i_square else circle
            imops.accum_inplace(
                im,
                im_marker,
                loc=XY(cy_rec.raw_x, cy_rec.raw_y),
                center=True,
            )

        elif show_circles:
            peak_records = df[(df.field_i == field_i)
                              & (df.cycle_i == cycle_i)]

            # In the case of a field with no peaks, n_peaks may be NaN, so check that we have
            # some peaks before passing NaNs to imops.
            if peak_records.n_peaks.iloc[0] > 0:
                for i, peak in peak_records.iterrows():
                    imops.accum_inplace(
                        im,
                        circle,
                        loc=XY(peak.raw_x, peak.raw_y),
                        center=True,
                    )

        z.im(
            im,
            f_title=f"ch_i={channel_i}  cy_i={cycle_i}  fl_i={field_i}",
            _full=True,
            _noaxes=True,
            _cspan=(float(min_bright), float(max_bright)),
        )
        displays.fix_auto_scroll()
Exemplo n.º 14
0
 def render(self, im, cy_i):
     super().render(im, cy_i)
     bg = np.random.normal(loc=self.bias, scale=self.std, size=self.dim)
     imops.accum_inplace(im, bg, XY(0, 0), center=False)
Exemplo n.º 15
0
 def it_adds_a_sub_image_into_a_target_with_clipping():
     dst = np.ones(WH(2, 2))
     src = np.ones(WH(4, 4))
     imops.accum_inplace(dst, src, XY(-1, -1))
     good = np.array([[2, 2], [2, 2]])
     assert (dst == good).all()
Exemplo n.º 16
0
    def __init__(
        self,
        n_peaks=1,  # Number of peaks to add
        n_cycles=1,  # Number of frames to make in the stack
        n_channels=1,  # Number of channels.
        dim=(512, 512),  # dims of frames
        bg_bias=0,  # bias of the background
        bg_std=0.5,  # std of the background noise per channel
        peak_focus=1.0,  # The std of the peaks (all will be the same)
        peak_mean=(
            40.0, ),  # Mean of the peak area distribution for each channel
        peak_std=2.0,  # Std of the peak areas distribution
        peak_xs=None,  # Used to put peaks at know locations
        peak_ys=None,  # Used to put peaks at know locations
        peak_area_all_cycles=None,  # Areas for all peaks on each cycle (len == n_cycles)
        peak_area_by_channel_cycle=None,  # Areas, all peaks, channels, cycles (len ==  n_channels * n_cycles * n_peaks)
        grid_distribution=False,  # When True the peaks are laid out in a grid
        all_aligned=False,  # When True all cycles will be aligned
        digitize=False,
        frame_offsets=None,
        anomalies=None,  # int, number of anomalies per image
        random_seed=None,
    ):

        if random_seed is not None:
            np.random.seed(random_seed)

        if not isinstance(bg_bias, tuple):
            bg_bias = tuple([bg_bias] * n_channels)

        if not isinstance(bg_std, tuple):
            bg_std = tuple([bg_std] * n_channels)

        self.bg_bias = bg_bias
        assert len(bg_bias) == n_channels

        self.bg_std = bg_std
        assert len(bg_std) == n_channels

        self.dim = HW(dim)

        self.anomalies = []

        # Peaks are a floating point positions (not perfectly aligned with pixels)
        # and the Gaussians are sampled around those points

        self.n_peaks = n_peaks
        self.n_cycles = n_cycles
        self.n_channels = n_channels
        self.peak_mean = peak_mean
        self.peak_std = peak_std
        self.peak_focus = peak_focus

        # unit_peak is only a reference; the real peaks are sub-pixel sampled but will have the same dimensions
        self.unit_peak = imops.generate_gauss_kernel(self.peak_focus)

        self.peak_dim = self.unit_peak.shape[0]

        if peak_xs is not None and peak_ys is not None:
            # Place peaks at specific locations
            self.peak_xs = np.array(peak_xs)
            self.peak_ys = np.array(peak_ys)
        else:
            # Place peaks in patterns or random
            if grid_distribution:
                # Put the peaks on a grid
                n_cols = int(math.sqrt(n_peaks) + 0.5)
                ixs = np.remainder(np.arange(n_peaks), n_cols)
                iys = np.arange(n_peaks) // n_cols
                border = 15
                self.peak_xs = (self.dim.w -
                                border * 2) * ixs / n_cols + border
                self.peak_ys = (self.dim.h -
                                border * 2) * iys / n_cols + border
            else:
                # Distribute the peaks randomly
                self.peak_xs = np.random.uniform(
                    low=self.peak_dim + 1.0,
                    high=self.dim.w - self.peak_dim - 1.0,
                    size=n_peaks,
                )
                self.peak_ys = np.random.uniform(
                    low=self.peak_dim + 1.0,
                    high=self.dim.h - self.peak_dim - 1.0,
                    size=n_peaks,
                )

        self.peak_locs = [XY(x, y) for x, y in zip(self.peak_xs, self.peak_ys)]
        if self.n_peaks > 0:
            peak_dists = distance.cdist(self.peak_locs, self.peak_locs,
                                        "euclidean")
            peak_dists[peak_dists == 0] = 10000.0
            self.closest = peak_dists.min(axis=0)

        self.peak_areas = np.empty((n_channels, n_cycles, n_peaks))
        if peak_area_all_cycles is not None:
            # Use one area for all peaks, all channels for each cycle
            assert len(peak_area_all_cycles) == n_cycles
            for cycle in range(n_cycles):
                self.peak_areas[:, cycle, :] = peak_area_all_cycles[cycle]
        elif peak_area_by_channel_cycle is not None:
            # Specified areas for each peak, each channel, each cycle
            assert peak_area_by_channel_cycle.shape == (n_channels, n_cycles,
                                                        n_peaks)
            self.peak_areas[:] = peak_area_by_channel_cycle[:]
        elif n_peaks > 0:
            # Make random peak areas by channel means
            for channel in range(n_channels):
                self.peak_areas[channel, :, :] = np.random.normal(
                    loc=self.peak_mean[channel],
                    scale=self.peak_std,
                    size=(n_cycles, n_peaks),
                )
                self.peak_areas[channel] = np.clip(self.peak_areas[channel],
                                                   0.0, 1000.0)

        # Frames are integer aligned because this is the best that the aligner would be able to do
        if frame_offsets is None:
            self.frame_xs = np.random.randint(low=-5, high=5, size=n_cycles)
            self.frame_ys = np.random.randint(low=-5, high=5, size=n_cycles)
        else:
            self.frame_xs = [i[1] for i in frame_offsets]
            self.frame_ys = [i[0] for i in frame_offsets]

        # Cycle 0 always has no offset
        self.frame_xs[0] = 0
        self.frame_ys[0] = 0

        if all_aligned:
            self.frame_xs = np.zeros((n_cycles, ))
            self.frame_ys = np.zeros((n_cycles, ))

        self.ims = np.zeros((n_channels, n_cycles, dim[0], dim[1]))
        for cycle in range(n_cycles):
            for channel in range(n_channels):
                # Background has bg_std plus bias
                im = bg_bias[channel] + np.random.normal(
                    size=dim, scale=self.bg_std[channel])

                # Peaks are on the pixel lattice from their floating point positions
                # No signal-proportional noise is added
                for x, y, a in zip(self.peak_xs, self.peak_ys,
                                   self.peak_areas[channel, cycle, :]):
                    # The center of the peak is at the floor pixel and the offset is the fractional part
                    _x = self.frame_xs[cycle] + x
                    _y = self.frame_ys[cycle] + y
                    ix = int(_x + 0.5)
                    iy = int(_y + 0.5)
                    frac_x = _x - ix
                    frac_y = _y - iy
                    g = imops.generate_gauss_kernel(self.peak_focus,
                                                    offset_x=frac_x,
                                                    offset_y=frac_y)
                    imops.accum_inplace(im, g * a, loc=XY(ix, iy), center=True)

                # overwrite with random anomalies if specified
                if anomalies is not None:
                    if cycle == 0:
                        # in cycle 0 pick location and size of anomalies for this image
                        anomalies_for_channel = []
                        self.anomalies += [anomalies_for_channel]
                        for i in range(anomalies):
                            sz = np.random.randint(10, 100, size=2)
                            l = np.random.randint(0, self.dim[0], size=2)
                            anomalies_for_channel += [(l, sz)]
                    for l, sz in self.anomalies[channel]:
                        # in all cycles, vary the location, size, and intensity of the anomaly,
                        # and print it to the image.
                        l = l * (1.0 + np.random.random() / 10.0)
                        sz = sz * (1.0 + np.random.random() / 10.0)
                        im[ROI(
                            l,
                            sz)] = im[ROI(l, sz)] * np.random.randint(2, 20)

                if digitize:
                    im = (im.clip(min=0) + 0.5).astype(
                        np.uint8)  # +0.5 to round up
                else:
                    im = im.clip(min=0)
                self.ims[channel, cycle, :, :] = im
Exemplo n.º 17
0
 def it_handles_smudged():
     true_aln_offsets, chcy_ims = _synth()
     smudge_im = np.full((400, 400), 1000)
     imops.accum_inplace(chcy_ims[0, 0], smudge_im, loc=XY(100, 100))
     pred_cy_offsets = align_ims(chcy_ims[0])
     assert np.all(np.abs(true_aln_offsets - pred_cy_offsets) < 0.16)
Exemplo n.º 18
0
def _step_3_composite_aligned_images(
    field_df, ch_merged_cy_ims, raw_chcy_ims, sigproc_params
):
    """
    Generate aligned images and composites
    """
    n_outchannels, n_inchannels, n_cycles, dim = sigproc_params.channels_cycles_dim

    # Note offsets are the same for each channel, and we only want one set of
    # offsets because we're aligning channel-merged images.
    offsets = [
        XY(row.shift_x, row.shift_y)
        for row in field_df[field_df.channel_i == 0]
        .set_index("cycle_i")
        .sort_index()[["shift_y", "shift_x"]]
        .itertuples()
    ]
    # Needs to be a list of Coords

    median_by_ch_cy = (
        field_df.set_index(["channel_i", "cycle_i"])
        .sort_index()
        .bg_median.values.reshape((n_outchannels, n_cycles))
    )

    chcy_composite_im, border_size = imops.composite(
        ch_merged_cy_ims,
        offsets,
        start_accum=sigproc_params.peak_find_start,
        limit_accum=sigproc_params.peak_find_n_cycles,
    )

    # GENERATE aligned images in the new coordinate system
    aligned_dim = HW(chcy_composite_im.shape)
    aligned_ims = np.zeros((n_outchannels, n_cycles, aligned_dim.h, aligned_dim.w,))
    aligned_raw_chcy_ims = np.zeros_like(aligned_ims)
    border = YX(border_size, border_size)
    for outch in range(n_outchannels):
        inch = sigproc_params.output_channel_to_input_channel(outch)
        for cy, offset in zip(range(n_cycles), offsets):
            imops.accum_inplace(
                aligned_raw_chcy_ims[outch, cy],
                src=raw_chcy_ims[inch, cy],
                loc=border - offset,
            )
            imops.accum_inplace(
                aligned_ims[outch, cy],
                src=(raw_chcy_ims[inch, cy] - median_by_ch_cy[outch, cy]).clip(min=0),
                loc=border - offset,
            )

    # BLACK out the borders by clipping in only pixels that are in every cycle
    l = border_size - field_df.shift_x.min()
    r = aligned_dim.w - border_size - field_df.shift_x.max()
    b = border_size - field_df.shift_y.min()
    t = aligned_dim.h - border_size - field_df.shift_y.max()
    roi = _roi_from_edges(b, t, l, r)
    aligned_roi_rect = Rect(b, t, l, r)

    aligned_composite_chcy_im = np.zeros(aligned_dim)
    aligned_composite_chcy_im[roi] = chcy_composite_im[roi]
    med = np.median(chcy_composite_im[roi])
    aligned_composite_bg_removed_im = (aligned_composite_chcy_im - med).clip(min=0)

    return (
        border_size,
        aligned_roi_rect,
        aligned_composite_bg_removed_im,
        aligned_raw_chcy_ims,
    )