def align(im_stack, return_shifted_ims=False, bounds=None): """ Align the image stack (1 pixel accuracy) relative to the first frame in the stack Arguments: im_stack (3 dimensions) return_shifted_ims: If True, also return the shifted images truncated to the common region of interest bounds: If not None limit the search space Returns: list of YX tuples shifted_ims (optional) """ check.array_t(im_stack, ndim=3, dtype=np.float64) n_cycles, mea_h, mea_w = im_stack.shape assert mea_h == mea_w offsets = [YX(0, 0)] primary = im_stack[0] for im in im_stack[1:]: # TODO: This could be optimized by using fft instead of # cv2.filter2D() which would avoid the fft of the # unchanging primary. conv = convolve(src=primary, kernel=im) # conv is now zero-centered; that is, the peak is # an offset relative to the center of the image. if bounds is not None: edge_fill(conv, (mea_w - 2 * bounds) // 2, val=0) peak = YX(np.unravel_index(conv.argmax(), conv.shape)) center = HW(conv.shape) // 2 offsets += [center - peak] if return_shifted_ims: raw_dim = im_stack.shape[-2:] roi = intersection_roi_from_aln_offsets(offsets, raw_dim) roi_dim = (roi[0].stop - roi[0].start, roi[1].stop - roi[1].start) pixel_aligned_cy_ims = np.zeros((n_cycles, mea_h, mea_w)) for cy_i, offset in zip(range(n_cycles), offsets): shifted_im = shift(im_stack[cy_i], offset * -1) pixel_aligned_cy_ims[cy_i, 0:roi_dim[0], 0:roi_dim[1]] = shifted_im[roi[0], roi[1]] return np.array(offsets), pixel_aligned_cy_ims else: return np.array(offsets)
def extract_trace(imstack, loc, dim, center=True): """Extract a trace of dim at loc from the stack""" imstack = stack(imstack) dim = HW(dim) loc = YX(loc) roi = ROI(loc, dim, center=center) return imstack[:, roi[0], roi[1]]
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)
def _pixel_to_subpixel_one_im(im, peak_dim, locs): """ This is a subtle calculation. locs is given as an *integer* position (only has pixel accuracy). We then extract out a sub-image using an *integer* half width. Peak_dim is typically odd. Suppose it is (11, 11) That makes half_peak_mea_i be 11 // 2 = 5 Suppose that a peak is at (17.5, 17.5). Suppose that peak was found a (integer) location (17, 17) which is within 1 pixel of its center as expected. We extract the sub-image at (17 - 5, 17 - 5) = (12:23, 12:23) The Center-of-mass calculation should return (5.5, 5.5) because that is relative to the sub-image which was extracted We wish to return (17.5, 17.5). So that's the lower left (17 - 5) of the peak plus the COM found. """ check.array_t(locs, dtype=int) assert peak_dim[0] == peak_dim[1] half_peak_mea_i = peak_dim[0] // 2 lower_left_locs = locs - half_peak_mea_i com_per_loc = np.zeros(locs.shape) for loc_i, loc in enumerate(lower_left_locs): peak_im = imops.crop(im, off=YX(loc), dim=peak_dim, center=False) com_per_loc[loc_i] = imops.com(peak_im**2) return lower_left_locs + com_per_loc
def it_shifts_with_extra_dims(): src = np.array([ [[1, 2, 3], [4, 5, 6], [7, 8, 9]], [[10, 20, 30], [40, 50, 60], [70, 80, 90]], ]) dst = imops.shift(src, YX(1, -1)) assert np.all(dst == [ [[0, 0, 0], [2, 3, 0], [5, 6, 0]], [[0, 0, 0], [20, 30, 0], [50, 60, 0]], ])
def fill(tar, loc, dim, val=0, center=False): """Fill target with value in ROI""" loc = YX(loc) dim = HW(dim) tar_dim = HW(tar.shape) if center: loc -= dim // 2 tar_roi, _ = clip2d(loc.x, tar_dim.w, dim.w, loc.y, tar_dim.h, dim.h) if tar_roi is not None: tar[tar_roi] = val
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)
def set_with_mask_in_place(tar, mask, value, loc=XY(0, 0), center=False): loc = YX(loc) tar_dim = HW(tar.shape) msk_dim = HW(mask.shape) if center: loc -= msk_dim // 2 tar_roi, msk_roi = clip2d(loc.x, tar_dim.w, msk_dim.w, loc.y, tar_dim.h, msk_dim.h) if tar_roi is not None and msk_roi is not None: subset = tar[tar_roi] subset[mask[msk_roi]] = value tar[tar_roi] = subset
def it_fits_a_gaussian(): xs, ys = np.mgrid[0:20, 0:20] data = imops._circ_gaussian(20.0, 10, 10, 1.0)( xs, ys) + 0.2 * np.random.random(xs.shape) center = YX(data.shape) / 2 g_params = imops.fit_circ_gaussian(data) assert -1.0 < (20.0 - g_params[0]) < 1.0 assert -1.0 < (10.0 - center.y - g_params[1]) < 1.0 assert -1.0 < (10.0 - center.x - g_params[2]) < 1.0 assert -0.15 < (1.0 - g_params[3]) < 0.15
def accum_inplace(tar, src, loc=XY(0, 0), center=False): """ Accumulate the src image into the (tar)get at loc with optional source centering """ loc = YX(loc) tar_dim = HW(tar.shape) src_dim = HW(src.shape) if center: loc -= src_dim // 2 tar_roi, src_roi = clip2d(loc.x, tar_dim.w, src_dim.w, loc.y, tar_dim.h, src_dim.h) if tar_roi is not None and src_roi is not None: tar[tar_roi] += src[src_roi]
def shift(src, loc=XY(0, 0)): """Offset""" loc = YX(loc) extra_dims = src.shape[0:-2] n_extra_dims = len(extra_dims) src_dim = HW(src.shape[-2:]) tar_dim = src_dim tar_roi, src_roi = clip2d(loc.x, tar_dim.w, src_dim.w, loc.y, tar_dim.h, src_dim.h) tar = np.zeros((*extra_dims, *tar_dim)) if tar_roi is not None and src_roi is not None: if n_extra_dims > 0: tar[:, tar_roi[0], tar_roi[1]] += src[:, src_roi[0], src_roi[1]] else: tar[tar_roi[0], tar_roi[1]] += src[src_roi[0], src_roi[1]] return tar
def align(im_stack): """ Align the stack relative to the first frame. I timed this versus a non-DFT solution and it was WAY better. Returns: list of YX tuples max_score """ check.array_t(im_stack, ndim=3) offsets = [YX(0, 0)] maxs = [0] primary = im_stack[0] for im in im_stack[1:]: conv = convolve(src=primary, kernel=im) # conv is now zero-centered; that is, the peak is # an offset relative to the center of the image. maxs += [np.amax(conv)] peak = YX(np.unravel_index(conv.argmax(), conv.shape)) center = HW(conv.shape) // 2 offsets += [center - peak] return np.array(offsets), np.array(maxs)
def composite(ims, offsets, start_accum=None, limit_accum=None): """Build up a composite image from the stack with offsets""" # FIND the largest offset and add a border around the image of that size border_size = np.abs(offsets).max() border = HW(border_size, border_size) comp_dim = HW(ims[0].shape) + border * 2 comp = np.zeros(comp_dim) comp_count = 0 for i, (im, offset) in enumerate(zip(ims, offsets)): if start_accum is not None and i < start_accum: continue if limit_accum is not None and comp_count >= limit_accum: break accum_inplace(comp, src=im, loc=border - YX(offset)) comp_count += 1 return comp, border_size
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)
def _quality(im): """ Measure the quality of an image by spatial low-pass filter. High quality images are one where there is very little low-frequency (but above DC) bands. """ a = np.copy(im) a -= np.mean(a) power = np.abs(np.fft.fftshift(np.fft.fft2(a))) power[power == 0] = 1 cen = YX(power.shape) / 2 dim_half = 3 dim = HW(dim_half * 2 + 1, dim_half * 2 + 1) roi = ROI(cen, dim, center=True) im = power[roi] eigen = imops.eigen_moments(im) score = power.sum() / np.sqrt(eigen.sum()) return score
def edge_fill(tar, loc, dim, val=0, center=False): """Fill rect edge target with value in ROI""" loc = YX(loc) dim = HW(dim) tar_dim = HW(tar.shape) if center: loc -= dim // 2 tar_roi, _ = clip2d(loc.x, tar_dim.w, dim.w, loc.y, tar_dim.h, dim.h) if tar_roi is not None: # Bottom tar[tar_roi[0].start, tar_roi[1].start : tar_roi[1].stop] = val # Top tar[tar_roi[0].stop - 1, tar_roi[1].start : tar_roi[1].stop] = val # Left tar[tar_roi[0].start : tar_roi[0].stop, tar_roi[1].start] = val # Right tar[tar_roi[0].start : tar_roi[0].stop, tar_roi[1].stop - 1] = val
def intersection_roi_from_aln_offsets(aln_offsets, raw_dim): """ Compute the ROI that contains pixels from all frames given the aln_offsets (returned from align) and the dim of the original images. """ aln_offsets = np.array(aln_offsets) check.affirm(np.all(aln_offsets[0] == (0, 0)), "intersection roi must start with (0,0)") # intersection_roi is the ROI in the coordinate space of # the [0] frame that has pixels from every cycle. clip_dim = ( np.min(aln_offsets[:, 0] + raw_dim[0]) - np.max(aln_offsets[:, 0]), np.min(aln_offsets[:, 1] + raw_dim[1]) - np.max(aln_offsets[:, 1]), ) b = max(0, -np.min(aln_offsets[:, 0])) t = min(raw_dim[0], b + clip_dim[0]) l = max(0, -np.min(aln_offsets[:, 1])) r = min(raw_dim[1], l + clip_dim[1]) return ROI(loc=YX(b, l), dim=HW(t - b, r - l))
def low_frequency_power(im, dim_half=3): """ Measure the low_frequency_power (excluding DC) of an image by spatial low-pass filter. dim_half is the half the width of the region """ a = np.copy(im) a -= np.mean(a) power = np.abs(np.fft.fftshift(np.fft.fft2(a))) power[power == 0] = 1 cen = YX(power.shape) / 2 dim = HW(dim_half * 2 + 1, dim_half * 2 + 1) # PLUCK out the center (which is the low frequencies) roi = ROI(cen, dim, center=True) im = power[roi] eigen = eigen_moments(im) score = power.sum() / np.sqrt(eigen.sum()) return score
def it_shifts_with_equal_ndims(): src = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) dst = imops.shift(src, YX(1, -1)) assert np.all(dst == [[0, 0, 0], [2, 3, 0], [5, 6, 0]])
def it_accepts_yx(): loc = YX(3, 2) assert loc.x == 2 and loc.y == 3 and loc == (3, 2)
def df_filter( df, fields=None, reject_fields=None, roi=None, channel_i=0, dark=None, on_through_cy_i=None, off_at_cy_i=None, monotonic=None, min_intensity_cy_0=None, max_intensity_cy_0=None, max_intensity_any_cycle=None, min_intensity_per_cycle=None, max_intensity_per_cycle=None, radmat_field="signal", max_k=None, min_score=None, ): """ A general filtering tool that operates on the dataframe returned by sigproc_v2.fields__n_peaks__peaks__radmat() """ n_channels = df.channel_i.max() + 1 # REMOVE unwanted fields if fields is None: fields = list(range(df.field_i.max() + 1)) if reject_fields is not None: fields = list(filter(lambda x: x not in reject_fields, fields)) _df = df[df.field_i.isin(fields)].reset_index(drop=True) # REMOVE unwanted peaks by ROI if roi is None: roi = ROI(YX(0, 0), HW(df.raw_y.max(), df.raw_x.max())) _df = _df[(roi[0].start <= _df.raw_y) & (_df.raw_y < roi[0].stop) & (roi[1].start <= _df.raw_x) & (_df.raw_x < roi[1].stop)].reset_index(drop=True) if max_k is not None: _df = _df[_df.k <= max_k] if min_score is not None: _df = _df[_df.score >= min_score] # OPERATE on radmat if needed fields_that_operate_on_radmat = [ dark, on_through_cy_i, off_at_cy_i, monotonic, min_intensity_cy_0, max_intensity_cy_0, max_intensity_any_cycle, min_intensity_per_cycle, max_intensity_per_cycle, ] if any([field is not None for field in fields_that_operate_on_radmat]): assert 0 <= channel_i < n_channels rad_pt = pd.pivot_table(_df, values=radmat_field, index=["peak_i"], columns=["channel_i", "cycle_i"]) ch_rad_pt = rad_pt.loc[:, channel_i] keep_peaks_mask = np.ones((ch_rad_pt.shape[0], ), dtype=bool) if on_through_cy_i is not None: assert dark is not None keep_peaks_mask &= np.all( ch_rad_pt.loc[:, 0:on_through_cy_i + 1] > dark, axis=1) if off_at_cy_i is not None: assert dark is not None keep_peaks_mask &= np.all(ch_rad_pt.loc[:, off_at_cy_i:] < dark, axis=1) if monotonic is not None: d = np.diff(ch_rad_pt.values, axis=1) keep_peaks_mask &= np.all(d < monotonic, axis=1) if min_intensity_cy_0 is not None: keep_peaks_mask &= ch_rad_pt.loc[:, 0] >= min_intensity_cy_0 if max_intensity_cy_0 is not None: keep_peaks_mask &= ch_rad_pt.loc[:, 0] <= max_intensity_cy_0 if max_intensity_any_cycle is not None: keep_peaks_mask &= np.all( ch_rad_pt.loc[:, :] <= max_intensity_any_cycle, axis=1) if min_intensity_per_cycle is not None: for cy_i, inten in enumerate(min_intensity_per_cycle): if inten is not None: keep_peaks_mask &= ch_rad_pt.loc[:, cy_i] >= inten if max_intensity_per_cycle is not None: for cy_i, inten in enumerate(max_intensity_per_cycle): if inten is not None: keep_peaks_mask &= ch_rad_pt.loc[:, cy_i] <= inten keep_peak_i = ch_rad_pt[keep_peaks_mask].index.values keep_df = pd.DataFrame( dict(keep_peak_i=keep_peak_i)).set_index("keep_peak_i") _df = keep_df.join(df.set_index("peak_i", drop=False)) return _df
def _roi_from_edges(b, t, l, r): return ROI(loc=YX(b, l), dim=HW(t - b, r - l))
def _psf_accumulate(im, locs, mea, keep_dist=8, threshold_abs=None, return_reasons=True): """ Given a single im, typically a regional sub-image, accumulate PSF evidence from each locs that meets a set of criteria. Any one image may not produce enough (or any) candidate spots and it is therefore expected that this function is called over a large number of fields to get sufficient samples. Arguments: im: Expected to be a single field, channel, cycle (BG already removed). locs: array (n, 2) in coordinates of im. Expected to be well-separated mea: The peak_measure (must be odd) threshold_abs: The average pixel brightness to accept the peak keep_dist: Pixels distance to determine crowding Returns: psf: ndarray (mea, mea) image reason_counts: An array of masks of why peaks were accepted/rejected See PSFEstimateMaskFields for the columns """ from scipy.spatial.distance import cdist # Defer slow import n_locs = len(locs) dist = cdist(locs, locs, metric="euclidean") dist[dist == 0.0] = np.nan if not np.all(np.isnan(dist)): closest_dist = np.nanmin(dist, axis=1) else: closest_dist = np.zeros(n_locs) # Aligned peaks will accumulate into this psf matrix dim = (mea, mea) dim2 = (mea + 2, mea + 2) psf = np.zeros(dim) n_reason_mask_fields = len(PSFEstimateMaskFields) reason_masks = np.zeros((n_locs, n_reason_mask_fields)) for i, (loc, closest_neighbor_dist) in enumerate(zip(locs, closest_dist)): reason_masks[i, PSFEstimateMaskFields.considered] = 1 # EXTRACT a peak with extra pixels around the edges (dim2 not dim) peak_im = imops.crop(im, off=YX(loc), dim=HW(dim2), center=True) if peak_im.shape != dim2: # Skip near edges reason_masks[i, PSFEstimateMaskFields.skipped_near_edges] = 1 continue if closest_neighbor_dist < keep_dist: reason_masks[i, PSFEstimateMaskFields.skipped_too_crowded] = 1 continue if np.any(np.isnan(peak_im)): reason_masks[i, PSFEstimateMaskFields.skipped_has_nan] = 1 continue # Sub-pixel align the peak to the center assert not np.any(np.isnan(peak_im)) centered_peak_im = sub_pixel_center(peak_im.astype(np.float64)) # Removing ckipping as the noise should cancel out # centered_peak_im = np.clip(centered_peak_im, a_min=0.0, a_max=None) peak_max = np.max(centered_peak_im) if peak_max == 0.0: reason_masks[i, PSFEstimateMaskFields.skipped_empty] = 1 continue if threshold_abs is not None and peak_max < threshold_abs: # Reject spots that are not active reason_masks[i, PSFEstimateMaskFields.skipped_too_dark] = 1 continue r = imops.distribution_aspect_ratio(centered_peak_im) if r > 2.0: reason_masks[i, PSFEstimateMaskFields.skipped_too_oval] = 1 continue # TRIM off the extra now centered_peak_im = centered_peak_im[1:-1, 1:-1] psf += centered_peak_im / np.sum(centered_peak_im) reason_masks[i, PSFEstimateMaskFields.accepted] = 1 n_accepted = np.sum(reason_masks[:, PSFEstimateMaskFields.accepted]) if n_accepted > 0: psf /= np.sum(psf) if return_reasons: return psf, reason_masks return psf
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, )
def it_shifts_an_roi(): roi = ROI(loc=YX(5, 10), dim=HW(15, 20)) new_roi = roi_shift(roi, YX(-2, -3)) assert new_roi == ROI(YX(5 - 2, 10 - 3), HW(15, 20))
def _radiometry(chcy_ims, locs, ch_z_reg_psfs, cycle_to_z_index): """ Use the PSFs to compute the Area-Under-Curve of the data in chcy_ims for each peak location of locs. Arguments: chcy_ims: (n_output_channels, n_cycles, width, height) locs: (n_peaks, 2). The second dimension is in (y, x) order ch_z_reg_psfs: (n_output_channels, n_z_slices, divs, divs, psf_mea, psf_mea) cycle_to_z_index: (n_cycles). This is the best z-slice of the ch_z_reg_psfs to use for each cycle determined by a focal fit. """ check.array_t(chcy_ims, ndim=4) check.array_t(locs, ndim=2, shape=(None, 2)) check.array_t(ch_z_reg_psfs, shape=(chcy_ims.shape[0], None, None, None, None, None)) check.array_t(cycle_to_z_index, shape=(chcy_ims.shape[1], )) n_locs = len(locs) n_channels, n_cycles = chcy_ims.shape[0:2] psf_divs = ch_z_reg_psfs.shape[2] assert psf_divs == ch_z_reg_psfs.shape[3] psf_dim = ch_z_reg_psfs.shape[-2:] psf_mea = psf_dim[0] assert psf_mea == psf_dim[1] radmat = np.full((n_locs, n_channels, n_cycles, 2), np.nan) # 2 is (sig, noi) center_weighted_mask = imops.generate_center_weighted_tanh(psf_mea, radius=2.0) for ch_i in range(n_channels): for cy_i in range(n_cycles): reg_psfs = ch_z_reg_psfs[ch_i, cycle_to_z_index[cy_i]] im = chcy_ims[ch_i, cy_i] for loc_i, loc in enumerate(locs): peak_im = imops.crop(im, off=YX(loc), dim=HW(psf_dim), center=True) if peak_im.shape != psf_dim: # Skip near edges continue if np.any(np.isnan(peak_im)): # Skip nan collisions continue # There is a small issue here -- when the regional PSFs # are computed they divide up the image over the full width # but the locs here are actually referring to the aligned # space which is typically a little smaller. This might # cause problems if alignment is very poor but is probably # too small of an effect to worry about in typical operations. psf_kernel = reg_psfs[int(psf_divs * loc[0] / im.shape[0]), int(psf_divs * loc[1] / im.shape[1]), ] signal, noise = _peak_radiometry( peak_im, psf_kernel, center_weighted_mask=center_weighted_mask) radmat[loc_i, ch_i, cy_i, :] = (signal, noise) return radmat