def it_clips2d_inclusive(): """ ssssssssssss sttttttttsss sttttttttsss ssssssssssss """ tar_roi, src_roi = tools.image.coord.clip2d(-1, 5, 10, -2, 4, 15) assert tar_roi == ROI(XY(0, 0), WH(5, 4)) assert src_roi == ROI(XY(1, 2), WH(5, 4))
def it_extracts_a_trace(): im_times_10 = im2 * 10 trace = imops.extract_trace( [im2, im_times_10], loc=XY(1, 0), dim=WH(2, 2), center=False ) expected = [[[2, 3], [5, 6]], [[20, 30], [50, 60]]] assert np.array_equal(trace, expected)
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)
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()
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 )
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)
def _mask_anomalies_im(im, den_threshold=300): """ Operates on pre-balanced images. The den_threshold of 300 was found empirically on Val data Sets anomalies to nan """ import skimage.transform # Defer slow imports import cv2 check.array_t(im, is_square=True) # SLICE into square using numpy-foo by reshaping the image # into a four-dimensional array can then by np.mean on the inner dimensions. sub_mea = 4 # Size of the sub-sample region im_mea, _ = im.shape squares = im.reshape(im_mea // sub_mea, sub_mea, im_mea // sub_mea, sub_mea) # At this point, im is now 4-dimensional like: (256, 2, 256, 2) # But we want the small_dims next to each other for simplicity so swap the inner axes squares = squares.swapaxes(1, 2) # Now squares is (256, 256, 2, 2.) # squares is like: 256, 256, 2, 2. So we need the mean of the last two axes squares = np.mean(squares, axis=(2, 3)) bad_mask = (squares > den_threshold).astype(float) # EXPAND the bad areas by erosion and dilate. # Erosion gets rid of the single-pixel hits and dilation expands the bad areas kernel = np.ones((3, 3), np.uint8) mask = cv2.erode(bad_mask, kernel, iterations=1) mask = cv2.dilate(mask, kernel, iterations=3) scale = im.shape[0] // mask.shape[0] full_size_mask = skimage.transform.rescale( mask, scale=scale, multichannel=False, mode="constant", anti_aliasing=False).astype(bool) # FIND rect contours of bad areas contours, hierarchy = cv2.findContours(full_size_mask.astype("uint8"), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) bad_rects = [cv2.boundingRect(cnt) for cnt in contours] im = im.copy() for rect in bad_rects: imops.fill(im, loc=XY(rect[0], rect[1]), dim=WH(rect[2], rect[3]), val=np.nan) return im
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 _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)
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 it_fills(): im = np.ones((3, 3)) im = imops.edge_fill(im, 1) # fmt: off assert im.tolist() == [ [0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 0.0] ] # fmt: on dst = np.ones(WH(4, 4)) imops.fill(dst, loc=XY(1, 1), dim=WH(10, 10)) good = np.array([[1, 1, 1, 1], [1, 0, 0, 0], [1, 0, 0, 0], [1, 0, 0, 0]]) assert (dst == good).all()
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 _mask_anomalies(cy_ims, bad_rects_by_cycle): """ Given a cycle stack of images and the list of bad rects, fill all these rects with background noise so that the aligner won't be confused by those anomalies. Arguments: cy_ims: array (n_cycles, height, width) bad_rects_by_cycle: List of bad rects for each cycle Returns: A copy of cy_ims with the bad rects masked with noise """ assert cy_ims.ndim == 3 and cy_ims.shape[1] == cy_ims.shape[2] n_cycles, _, _ = cy_ims.shape masked_ims = np.zeros_like(cy_ims) for cy in range(n_cycles): src_im = cy_ims[cy] bad_rects = bad_rects_by_cycle[cy] # MAKE a mask_im with 0 inside bad rects, 1 otherwise mask_im = np.ones_like(src_im) for rect in bad_rects: imops.fill(mask_im, loc=XY(rect[0], rect[1]), dim=WH(rect[2], rect[3]), val=0) # FIND the characteristics of a normal distribution that fits the # data that is not masked out (that is, we don't want the anomalies # in this distribution). If src_im is entirely masked, mean=std=0. # TASK: This could be accelerated by subsampling. mean = std = 0 if np.any(mask_im): mean, std = norm.fit(src_im[mask_im > 0]) bg_noise = norm.rvs(loc=mean, scale=std, size=src_im.shape, random_state=None) masked_ims[cy] = np.where(mask_im < 1, bg_noise, src_im) return masked_ims
def it_has_x_y_w_h_properties(): loc = XY(1, 2) assert loc.x == 1 and loc.y == 2 and loc.w == 1 and loc.h == 2
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 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)
def _subsize_sub_pixel_align_cy_ims(pixel_aligned_cy_ims, subsize, n_samples): """ The inner loop of _sub_pixel_align_cy_ims() that executes on a "subsize" region of the larger image. Is subsize is None then it uses the entire image. """ n_max_failures = n_samples * 2 sub_pixel_offsets = np.zeros((n_samples, pixel_aligned_cy_ims.shape[0], 2)) pixel_aligned_cy0_im = pixel_aligned_cy_ims[0] im_mea = pixel_aligned_cy_ims.shape[-1] assert pixel_aligned_cy_ims.shape[-2] == im_mea def _subregion(im, pos): if subsize is None: return im else: return imops.crop(im, off=pos, dim=WH(subsize, subsize), center=False) sample_i = 0 n_failures = 0 while sample_i < n_samples and n_failures < n_max_failures: try: if subsize is None: pos = XY(0, 0) else: pos = XY( np.random.randint(0, im_mea - subsize - 16), np.random.randint(0, im_mea - subsize - 16), ) subregion_pixel_aligned_cy0_im = _subregion( pixel_aligned_cy0_im, pos) for cy_i, pixel_aligned_cy_im in enumerate(pixel_aligned_cy_ims): if cy_i == 0: continue # Use a small region to improve speed subregion_pixel_aligned_cy_im = _subregion( pixel_aligned_cy_im, pos) try: _dy, _dx = _subpixel_align_one_im( subregion_pixel_aligned_cy0_im, subregion_pixel_aligned_cy_im, ) sub_pixel_offsets[sample_i, cy_i, :] = (_dy, _dx) except Exception: # This is a general exception handler because there # are a number of ways that the _subpixel_align_one_im # can fail including linear algebera, etc. All # of which end up with a skip and a retry. n_failures += 1 raise AlignmentError sample_i += 1 except AlignmentError: # Try again with a new pos if n_failures >= n_max_failures: raise AlignmentError return np.mean(sub_pixel_offsets, axis=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, )
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()
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_4_find_peaks( aligned_composite_bg_removed_im, aligned_roi_rect, raw_mask_rects, border_size, field_df, sigproc_params, ): """ Find peaks on the composite image TASK: Remove the mask rect checks and replace with the same masking logic that is now implemented in the alignment phase. That is, just remove the peaks from the source instead of in post-processing. """ from skimage.feature import peak_local_max # Defer slow import from scipy.stats import iqr n_outchannels, n_inchannels, n_cycles, dim = sigproc_params.channels_cycles_dim assert ( aligned_composite_bg_removed_im.shape[0] == aligned_composite_bg_removed_im.shape[1] ) aligned_dim, _ = aligned_composite_bg_removed_im.shape check.array_t(aligned_composite_bg_removed_im, is_square=True) hat_rad = sigproc_params.hat_rad brim_rad = sigproc_params.hat_rad + 1 hat_mask, brim_mask = _hat_masks(hat_rad, brim_rad) kernel = imops.generate_gauss_kernel(1.0) kernel = kernel - kernel.mean() _fiducial_im = imops.convolve(aligned_composite_bg_removed_im, kernel) # Black out the convolution artifact around the perimeter of the _fiducial_im search_roi_rect = Rect( aligned_roi_rect.b + brim_rad, aligned_roi_rect.t - brim_rad, aligned_roi_rect.l + brim_rad, aligned_roi_rect.r - brim_rad, ) search_roi = search_roi_rect.roi() composite_fiducial_im = np.zeros_like(aligned_composite_bg_removed_im) # Use Inter-Quartile Range for some easy filtering _iqr = 0 if sigproc_params.iqr_rng is not None: _iqr = iqr( _fiducial_im[search_roi], rng=(100 - sigproc_params.iqr_rng, sigproc_params.iqr_rng), ) composite_fiducial_im[search_roi] = (_fiducial_im[search_roi] - _iqr).clip(min=0) locs = peak_local_max( composite_fiducial_im, min_distance=hat_rad, threshold_abs=sigproc_params.threshold_abs, ) # Emergency exit to prevent memory overflows # check.affirm(len(locs) < 7000, f"Too many peaks {len(locs)}") shift = field_df.set_index("cycle_i").sort_index()[["shift_y", "shift_x"]].values shift_y = shift[:, 0] shift_x = shift[:, 1] # Discard any peak in any mask_rect # ALIGN the mask rects to the composite coordinate system aligned_mask_rects = [] for channel in range(sigproc_params.n_output_channels): channel_rects = safe_list_get(raw_mask_rects, channel, []) for cycle in range(n_cycles): for rect in safe_list_get(channel_rects, cycle, []): yx = XY(rect[0], rect[1]) hw = WH(rect[2], rect[3]) yx += XY(border_size, border_size) - XY(shift_x[cycle], shift_y[cycle]) aligned_mask_rects += [(yx[0], yx[1], yx[0] + hw[0], yx[1] + hw[1])] aligned_mask_rects = np.array(aligned_mask_rects) if aligned_mask_rects.shape[0] > 0: # To compare every loc with every mask rect we use the tricky np.fn.outer() y_hits = np.greater_equal.outer(locs[:, 0], aligned_mask_rects[:, 0]) y_hits &= np.less.outer(locs[:, 0], aligned_mask_rects[:, 2]) x_hits = np.greater_equal.outer(locs[:, 1], aligned_mask_rects[:, 1]) x_hits &= np.less.outer(locs[:, 1], aligned_mask_rects[:, 3]) inside_rect = x_hits & y_hits # inside a rect if x and y are inside the rect locs_to_keep = ~np.any( inside_rect, axis=1 ) # Reject if inside of any masked rect locs = locs[locs_to_keep] circle_im = np.zeros((aligned_dim, aligned_dim)) center = aligned_dim / 2 peak_rows = [] for field_peak_i, loc in enumerate(locs): if sigproc_params.radial_filter is not None: radius = math.sqrt((loc[0] - center) ** 2 + (loc[1] - center) ** 2) radius /= center if radius >= sigproc_params.radial_filter: continue imops.set_with_mask_in_place(circle_im, brim_mask, 1, loc=loc, center=True) peak_rows += [ Munch( peak_i=0, field_peak_i=field_peak_i, aln_y=int(loc[0]), aln_x=int(loc[1]), ) ] peak_df = pd.DataFrame(peak_rows) return peak_df, circle_im, aligned_mask_rects
def it_can_can_slice_and_dice_with_roi(): dim = WH(10, 10) image = np.zeros(dim) roi = ROI(XY(1, 1), dim - WH(2, 2)) cropped_image = image[roi] assert np.array_equal(image[1:9, 1:9], cropped_image[:, :])
def it_can_can_slice_an_roi_with_centering(): roi = ROI(XY(1, 2), WH(2, 4), center=True) assert roi[0] == slice(0, 4) and roi[1] == slice(0, 2)
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, )