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 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 _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
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 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 peak_find_chcy_ims(chcy_ims, approx_psf, cycle_i, subpixel=True): """ Previous version of this code depended on the channels being balanced relative to one another. But this early-stage channel balancing turned out to be problematic. This new code instead peak finds on each channel independently and then reconciles the peak locations by unioning the lists of peaks and de-duping them using an approximate measure of distance as the de-dupe key. If subpixel is not True uses the faster pixel-only accuracy. Returns: locs: ndarray (n_peaks, 2) where the second dim is in (y, x) order """ # Use more than one cycle to improve the quality of the sub-pixel estimate # But then discard peaks that are off after cycle 1?? n_channels = chcy_ims.shape[0] locs_per_channel = [] for ch_i in range(n_channels): im = chcy_ims[ch_i, cycle_i, :, :] try: locs = pixel_peak_find_one_im(im, approx_psf) if subpixel: locs = _pixel_to_subpixel_one_im(im, HW(approx_psf.shape), locs.astype(int)) except Exception: # Failure during peak find, no peaks recorded for this frame. locs = np.zeros((0, 2)) locs_per_channel += [locs] union_locs = np.vstack(locs_per_channel) # UNION and de-dupe where <= 1.0 pixel is considered a duplicate dists = cdist(union_locs, union_locs, "euclidean") n_locs = union_locs.shape[0] if n_locs == 0: return np.zeros((0, 2)) # Set self-distances to large so that they will not be found as "closest" dists[np.arange(n_locs), np.arange(n_locs)] = 1e6 closest_i = np.argmin(dists, axis=1) closest_d = dists[np.arange(n_locs), closest_i] # Any peaks with another peak within 1 pixel is a candidate for de-dupeing dedupe_mask = closest_d <= 1.0 a_iz = closest_i[dedupe_mask] b_iz = np.arange(n_locs)[dedupe_mask] c_iz = np.arange(n_locs)[ ~dedupe_mask] # Those with no mate (in one channel only) # Of those pairs we have to keep one or the other so we take the # one with the lower index value. keep_iz = np.where(a_iz < b_iz, a_iz, b_iz) keep_iz = np.concatenate((keep_iz, c_iz)) return union_locs[np.unique(keep_iz)]
def it_finds_sub_pixel_exactly_under_ideal_conditions(): """ Test the helper _sub_pixel_peak_find instead of sub_pixel_peak_find because we don't want to have to reconcile the peak ordering from the synth with the arbitrary order they are found by the peak finder """ with synth.Synth(dim=(512, 512), n_cycles=3) as s: true_n_peaks = 100 peaks = ( synth.PeaksModelGaussianCircular(n_peaks=true_n_peaks) .amps_constant(1000) .widths_uniform(1.5) .locs_grid() .locs_add_random_subpixel() ) s.zero_aln_offsets() chcy_ims = s.render_chcy() chcy_mean_im = np.mean(chcy_ims, axis=(0, 1)) locs = peak_find._pixel_to_subpixel_one_im( chcy_mean_im, HW(peaks.mea, peaks.mea), peaks.locs.astype(int) ) dists = np.linalg.norm(locs - peaks.locs, axis=1) assert np.all(dists < 0.01)
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 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 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 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 it_finds_one_peak_sub_pixel_exactly_under_ideal_conditions(): true_locs = np.array([[17.5, 17.5]]) peak_im = imops.gauss2_rho_form( amp=1000.0, std_x=1.8, std_y=1.8, pos_x=true_locs[0, 1], pos_y=true_locs[0, 0], rho=0.0, const=0.0, mea=35, ) pred_locs = peak_find._pixel_to_subpixel_one_im( peak_im, HW(11, 11), true_locs.astype(int) ) dists = np.linalg.norm(true_locs - pred_locs, axis=1) assert np.all(dists < 0.001)
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 it_finds_one_peak_sub_pixel_exactly_under_ideal_conditions_many_offsets(): for trials in range(50): true_locs = np.random.uniform(-5, 5, size=(1, 2)) true_locs += 35 / 2 peak_im = imops.gauss2_rho_form( amp=1000.0, std_x=1.8, std_y=1.8, pos_x=true_locs[0, 1], pos_y=true_locs[0, 0], rho=0.0, const=0.0, mea=35, ) pred_locs = peak_find._pixel_to_subpixel_one_im( peak_im, HW(11, 11), true_locs.astype(int) ) dists = np.linalg.norm(true_locs - pred_locs, axis=1) assert np.all(dists < 0.001)
def peak_find_chcy_ims_fast(chcy_ims, approx_psf, cycle_i, subpixel=True): """ Unlike the above this assumes that channel balance is working well and that we can just mean over the channels """ n_channels = chcy_ims.shape[0] np.save(f"/erisyon/internal/_chcy_ims_{cycle_i}.npy", chcy_ims[:, cycle_i, 0, 0]) im = np.mean(chcy_ims[:, cycle_i, :, :], axis=0) np.save(f"/erisyon/internal/_mean_im_{cycle_i}.npy", im) try: locs = pixel_peak_find_one_im(im, approx_psf) if subpixel: locs = _pixel_to_subpixel_one_im(im, HW(approx_psf.shape), locs.astype(int)) except Exception: # Failure during peak find, no peaks recorded for this frame. locs = np.zeros((0, 2)) return locs
def it_finds_sub_pixel_well_under_typical_conditions(): bg_std = 10 with synth.Synth(dim=(512, 512), n_cycles=3) as s: true_n_peaks = 100 peaks = ( synth.PeaksModelGaussianCircular(n_peaks=true_n_peaks) .amps_constant(1000) .widths_uniform(1.5) .locs_randomize_away_from_edges() ) synth.CameraModel(0, bg_std) s.zero_aln_offsets() chcy_ims = s.render_chcy() chcy_mean_im = np.mean(chcy_ims, axis=(0, 1)) locs = peak_find._pixel_to_subpixel_one_im( chcy_mean_im, HW(peaks.mea, peaks.mea), peaks.locs.astype(int) ) dists = np.linalg.norm(locs - peaks.locs, axis=1) assert (dists < 0.1).sum() > 30 assert (dists < 0.2).sum() > 70
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 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 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 crop(src, off=XY(0, 0), dim=WH(-1, -1), center=False): if dim.h == -1 and dim.w == -1: dim = HW(src.shape) return src[ROI(off, dim, center=center)]
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_accepts_hw(): dim = HW(3, 2) assert dim.x == 2 and dim.y == 3 and dim == (3, 2)
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 __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
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 _im_setup(self, im, fig): from bokeh.models import LinearColorMapper # Defer slow imports _im = im assert _im.ndim == 2 ustack = self._u_stack() y = 0 if ustack.get("_flip_y"): assert "f_y_range" not in ustack _im = np.flip(_im, axis=0) y = _im.shape[0] dim = HW(_im.shape) if dim.w == 0 or dim.h == 0: # Deal with zero dims gracefully dim = HW(max(1, dim.h), max(1, dim.w)) _im = np.zeros(dim) full_w, full_h = (ustack.get("_full_w", False), ustack.get("_full_h", False)) if ustack.get("_full"): full_w, full_h = (True, True) if full_h: full_h = dim.h + 40 # TASK: This value is weird, need a way to derive it if ustack.get("_min_h") is not None: full_h = max(full_h, ustack.get("_min_h")) if full_w: full_w = dim.w + 20 # TASK: This value is weird, need a way to derive it if ustack.get("_min_w") is not None: full_w = max(full_w, ustack.get("_min_w")) _nan = ustack.get("_nan") if _nan is not None: _im = np.nan_to_num(_im) _cper = ustack.get("_cper") if _cper is not None: # color by percentile assert isinstance(_cper, tuple) and len(_cper) == 2 low, high = np.nanpercentile(_im, _cper) else: _cspan = ustack.get("_cspan") if _cspan is None: low = 0.0 if _im.shape[0] == 0: high = 1.0 else: with warnings.catch_warnings(): warnings.filterwarnings("error") try: high = np.nanmean(_im) + np.nanstd(_im) * 4 except Exception: high = 1.0 else: if isinstance(_cspan, (list, tuple)): assert len(_cspan) == 2 low = _cspan[0] high = _cspan[1] elif isinstance(_cspan, np.ndarray): assert _cspan.shape == (2,) low = _cspan[0] high = _cspan[1] else: assert isinstance(_cspan, (int, float)) low = 0 high = _cspan pal = ustack.get("_palette", "viridis") if pal == "viridis": from bokeh.palettes import viridis pal = viridis(256) elif pal in ("gray", "grey",): from bokeh.palettes import gray pal = gray(256) elif pal == "inferno": from bokeh.palettes import inferno pal = inferno(256) # zero_color = ustack.get("_zero_color") # if zero_color is not None: # pal = copy.copy(pal) # pal[0] = zero_color cmap = LinearColorMapper(palette=pal, low=low, high=high) if full_w: fig.plot_width = full_w if full_h: fig.plot_height = full_h return dim, cmap, _im, y
def it_enlarged_the_canvas_to_maximum_offset(): hw = HW(comp.shape) assert ( hw.w == 62 and hw.h == 62 ) # 62 because the max offset is 6. So 6*2 (each side) + 50 original dim
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