def plot_2d_shadows(self, roi, flipz=False): """Plots 2D shadows in each axis around the 3D visualization. Args: roi (:class:`numpy.ndarray`): Region of interest. flipz (bool): True to invert ``roi`` along z-axis to match handedness of Matplotlib with z progressing upward; defaults to False. """ # set up shapes, accounting for any isotropic resizing if flipz: # invert along z-axis to match handedness of Matplotlib with z up roi = roi[::-1] if len(roi.shape) > 2: # covert 4D to 3D array, using only the 1st channel roi = roi[:, :, :, 0] isotropic = plot_3d.get_isotropic_vis(config.roi_profile) shape = roi.shape shape_iso = np.multiply(roi.shape, isotropic).astype(np.int) shape_iso_mid = shape_iso // 2 # TODO: shift z by +10? # xy-plane, positioned just below the 3D ROI img2d = roi[shape[0] // 2, :, :] img2d = transform.resize(img2d, np.multiply(img2d.shape, isotropic[1:]).astype(np.int), preserve_range=True) img2d_mlab = self._shadow_img2d(img2d, shape_iso, 0) # Mayavi positions are in x,y,z img2d_mlab.actor.position = [shape_iso_mid[2], shape_iso_mid[1], -10] # xz-plane img2d = roi[:, shape[1] // 2, :] img2d = transform.resize(img2d, np.multiply(img2d.shape, isotropic[[0, 2]]).astype(np.int), preserve_range=True) img2d_mlab = self._shadow_img2d(img2d, shape_iso, 2) img2d_mlab.actor.position = [-10, shape_iso_mid[1], shape_iso_mid[0]] img2d_mlab.actor.orientation = [90, 90, 0] # yz-plane img2d = roi[:, :, shape[2] // 2] img2d = transform.resize(img2d, np.multiply(img2d.shape, isotropic[:2]).astype(np.int), preserve_range=True) img2d_mlab = self._shadow_img2d(img2d, shape_iso, 1) img2d_mlab.actor.position = [shape_iso_mid[2], -10, shape_iso_mid[0]] img2d_mlab.actor.orientation = [90, 0, 0]
def plot_3d_points(self, roi, channel, flipz=False, offset=None): """Plots all pixels as points in 3D space. Points falling below a given threshold will be removed, allowing the viewer to see through the presumed background to masses within the region of interest. Args: roi (:class:`numpy.ndarray`): Region of interest either as a 3D ``z,y,x`` or 4D ``z,y,x,c`` array. channel (int): Channel to select, which can be None to indicate all channels. flipz (bool): True to invert the ROI along the z-axis to match the handedness of Matplotlib with z progressing upward; defaults to False. offset (Sequence[int]): Origin coordinates in ``z,y,x``; defaults to None. Returns: bool: True if points were rendered, False if no points to render. """ print("Plotting ROI as 3D points") # streamline the image if roi is None or roi.size < 1: return False roi = plot_3d.saturate_roi(roi, clip_vmax=98.5, channel=channel) roi = np.clip(roi, 0.2, 0.8) roi = restoration.denoise_tv_chambolle(roi, weight=0.1) # separate parallel arrays for each dimension of all coordinates for # Mayavi input format, with the ROI itself given as a 1D scalar array ; # TODO: consider using np.mgrid to construct the x,y,z arrays time_start = time() shape = roi.shape isotropic = plot_3d.get_isotropic_vis(config.roi_profile) z = np.ones((shape[0], shape[1] * shape[2])) for i in range(shape[0]): z[i] = z[i] * i if flipz: # invert along z-axis to match handedness of Matplotlib with z up z *= -1 if offset is not None: offset = np.copy(offset) offset[0] *= -1 y = np.ones((shape[0] * shape[1], shape[2])) for i in range(shape[0]): for j in range(shape[1]): y[i * shape[1] + j] = y[i * shape[1] + j] * j x = np.ones((shape[0] * shape[1], shape[2])) for i in range(shape[0] * shape[1]): x[i] = np.arange(shape[2]) if offset is not None: offset = np.multiply(offset, isotropic) coords = [z, y, x] for i, _ in enumerate(coords): # scale coordinates for isotropy coords[i] *= isotropic[i] if offset is not None: # translate by offset coords[i] += offset[i] multichannel, channels = plot_3d.setup_channels(roi, channel, 3) for chl in channels: roi_show = roi[..., chl] if multichannel else roi roi_show_1d = roi_show.reshape(roi_show.size) if chl == 0: x = np.reshape(x, roi_show.size) y = np.reshape(y, roi_show.size) z = np.reshape(z, roi_show.size) settings = config.get_roi_profile(chl) # clear background points to see remaining structures thresh = 0 if len(np.unique(roi_show)) > 1: # need > 1 val to threshold try: thresh = filters.threshold_otsu(roi_show, 64) except ValueError as e: thresh = np.median(roi_show) print("could not determine Otsu threshold, taking median " "({}) instead".format(thresh)) thresh *= settings["points_3d_thresh"] print("removing 3D points below threshold of {}".format(thresh)) remove = np.where(roi_show_1d < thresh) roi_show_1d = np.delete(roi_show_1d, remove) # adjust range from 0-1 to region of colormap to use roi_show_1d = libmag.normalize(roi_show_1d, 0.6, 1.0) points_len = roi_show_1d.size if points_len == 0: print("no 3D points to display") return False mask = math.ceil(points_len / self._MASK_DIVIDEND) print("points: {}, mask: {}".format(points_len, mask)) if any(np.isnan(roi_show_1d)): # TODO: see if some NaNs are permissible print( "NaN values for 3D points, will not show 3D visualization") return False pts = self.scene.mlab.points3d(np.delete(x, remove), np.delete(y, remove), np.delete(z, remove), roi_show_1d, mode="sphere", scale_mode="scalar", mask_points=mask, line_width=1.0, vmax=1.0, vmin=0.0, transparent=True) cmap = colormaps.get_cmap(config.cmaps, chl) if cmap is not None: pts.module_manager.scalar_lut_manager.lut.table = cmap( range(0, 256)) * 255 # scale glyphs to partially fill in gaps from isotropic scaling; # do not use actor scaling as it also translates the points when # not positioned at the origin pts.glyph.glyph.scale_factor = 2 * max(isotropic) # keep visual ordering of surfaces when opacity is reduced self.scene.renderer.use_depth_peeling = True print("time for 3D points display: {}".format(time() - time_start)) return True
def show_blobs(self, segments, segs_in_mask, cmap, roi_offset, roi_size, show_shadows=False, flipz=None): """Show 3D blobs as points. Args: segments: Labels from 3D blob detection method. segs_in_mask: Boolean mask for segments within the ROI; all other segments are assumed to be from padding and border regions surrounding the ROI. cmap (:class:`numpy.ndaarry`): Colormap as a 2D Numpy array in the format ``[[R, G, B, alpha], ...]``. roi_offset (Sequence[int]): Region of interest offset in ``z,y,x``. roi_size (Sequence[int]): Region of interest size in ``z,y,x``. Used to show the ROI outline. show_shadows: True if shadows of blobs should be depicted on planes behind the blobs; defaults to False. flipz (bool): True to invert blobs along the z-axis to match the handedness of Matplotlib with z progressing upward; defaults to False. Returns: A 3-element tuple containing ``pts_in``, the 3D points within the ROI; ``cmap'', the random colormap generated with a color for each blob, and ``scale``, the current size of the points. """ if segments.shape[0] <= 0: return None, 0 if roi_offset is None: roi_offset = np.zeros(3, dtype=np.int) if self.blobs: for blob in self.blobs: # remove existing blob glyphs from the pipeline blob.remove() settings = config.roi_profile # copy blobs with duplicate columns to access original values for # the coordinates callback when a blob is selected segs = np.concatenate((segments[:, :4], segments[:, :4]), axis=1) isotropic = plot_3d.get_isotropic_vis(settings) if flipz: # invert along z-axis within the same original space, eg to match # handedness of Matplotlib with z up segs[:, 0] *= -1 roi_offset = np.copy(roi_offset) roi_offset[0] *= -1 roi_size = np.copy(roi_size) roi_size[0] *= -1 segs[:, :3] = np.add(segs[:, :3], roi_offset) if isotropic is not None: # adjust position based on isotropic factor roi_offset = np.multiply(roi_offset, isotropic) roi_size = np.multiply(roi_size[:3], isotropic) segs[:, :3] = np.multiply(segs[:, :3], isotropic) radii = segs[:, 3] scale = 5 if radii is None else np.mean( np.mean(radii) + np.amax(radii)) print("blob point scaling: {}".format(scale)) # colormap has to be at least 2 colors segs_in = segs[segs_in_mask] cmap_indices = np.arange(segs_in.shape[0]) if show_shadows: # show projections onto side planes, assumed to be at -10 units # along the given axis segs_ones = np.ones(segs.shape[0]) # xy self._shadow_blob(segs_in[:, 2], segs_in[:, 1], segs_ones * -10, cmap_indices, cmap, scale) # xz shadows = self._shadow_blob(segs_in[:, 2], segs_in[:, 0], segs_ones * -10, cmap_indices, cmap, scale) shadows.actor.actor.orientation = [90, 0, 0] shadows.actor.actor.position = [0, -20, 0] # yz shadows = self._shadow_blob(segs_in[:, 1], segs_in[:, 0], segs_ones * -10, cmap_indices, cmap, scale) shadows.actor.actor.orientation = [90, 90, 0] shadows.actor.actor.position = [0, 0, 0] # show blobs within the ROI points_len = len(segs) mask = math.ceil(points_len / self._MASK_DIVIDEND) print("points: {}, mask: {}".format(points_len, mask)) pts_in = None self.blobs = [] if len(segs_in) > 0: # each Glyph contains multiple 3D points, one for each blob pts_in = self.scene.mlab.points3d(segs_in[:, 2], segs_in[:, 1], segs_in[:, 0], cmap_indices, mask_points=mask, scale_mode="none", scale_factor=scale, resolution=50) pts_in.module_manager.scalar_lut_manager.lut.table = cmap self.blobs.append(pts_in) # show blobs within padding or border region as black and more # transparent segs_out_mask = np.logical_not(segs_in_mask) if np.sum(segs_out_mask) > 0: self.blobs.append( self.scene.mlab.points3d(segs[segs_out_mask, 2], segs[segs_out_mask, 1], segs[segs_out_mask, 0], color=(0, 0, 0), mask_points=mask, scale_mode="none", scale_factor=scale / 2, resolution=50, opacity=0.2)) def pick_callback(pick): # handle picking blobs/glyphs if pick.actor in pts_in.actor.actors: # get the blob corresponding to the picked glyph actor blobi = pick.point_id // glyph_points.shape[0] else: # find the closest blob to the pick position dists = np.linalg.norm(segs_in[:, :3] - pick.pick_position[::-1], axis=1) blobi = np.argmin(dists) if dists[blobi] > max_dist: # remove blob if not within a tolerated distance blobi = None if blobi is None: # revert outline to full ROI if no blob is found self.show_roi_outline(roi_offset, roi_size) else: # move outline cube to surround picked blob; each glyph has # has many points, and each point ID maps to a data index # after floor division by the number of points z, y, x, r = segs_in[blobi, :4] outline.bounds = (x - r, x + r, y - r, y + r, z - r, z + r) if self.fn_update_coords: # callback to update coordinates using blob's orig coords self.fn_update_coords( np.add(segs_in[blobi, 4:7], roi_offset).astype(np.int)) # show ROI outline and make blobs pickable, falling back to closest # blobs within 20% of the longest ROI edge to be picked if present outline = self.show_roi_outline(roi_offset, roi_size) print(outline) glyph_points = pts_in.glyph.glyph_source.glyph_source.output.points.\ to_array() max_dist = max(roi_size) * 0.2 self.scene.mlab.gcf().on_mouse_pick(pick_callback) return pts_in, scale
def plot_3d_surface(self, roi, channel, segment=False, flipz=False, offset=None): """Plots areas with greater intensity as 3D surfaces. The scene will be cleared before display. Args: roi (:class:`numpy.ndarray`): Region of interest either as a 3D ``z,y,x`` or 4D ``z,y,x,c`` array. channel (int): Channel to select, which can be None to indicate all channels. segment (bool): True to denoise and segment ``roi`` before displaying, which may remove artifacts that might otherwise lead to spurious surfaces. Defaults to False. flipz: True to invert ``roi`` along z-axis to match handedness of Matplotlib with z progressing upward; defaults to False. offset (Sequence[int]): Origin coordinates in ``z,y,x``; defaults to None. Returns: list: List of Mayavi surfaces for each displayed channel, which are also stored in :attr:`surfaces`. """ # Plot in Mayavi print("viewing 3D surface") pipeline = self.scene.mlab.pipeline settings = config.roi_profile if flipz: # invert along z-axis to match handedness of Matplotlib with z up roi = roi[::-1] if offset is not None: # invert z-offset and translate by ROI z-size so ROI is # mirrored across the xy-plane offset = np.copy(offset) offset[0] = -offset[0] - roi.shape[0] isotropic = plot_3d.get_isotropic_vis(settings) # saturate to remove noise and normalize values roi = plot_3d.saturate_roi(roi, channel=channel) # turn off segmentation if ROI too big (arbitrarily set here as # > 10 million pixels) to avoid performance hit and since likely showing # large region of downsampled image anyway, where don't need hi res num_pixels = np.prod(roi.shape) to_segment = num_pixels < 10000000 time_start = time() multichannel, channels = plot_3d.setup_channels(roi, channel, 3) surfaces = [] for chl in channels: roi_show = roi[..., chl] if multichannel else roi # clip to minimize sub-nuclear variation roi_show = np.clip(roi_show, 0.2, 0.8) if segment: # denoising makes for much cleaner images but also seems to # allow structures to blend together # TODO: consider segmenting individual structures and rendering # as separate surfaces to avoid blending roi_show = restoration.denoise_tv_chambolle(roi_show, weight=0.1) # build surface from segmented ROI if to_segment: vmin, vmax = np.percentile(roi_show, (40, 70)) walker = segmenter.segment_rw(roi_show, chl, vmin=vmin, vmax=vmax) roi_show *= np.subtract(walker[0], 1) else: print("deferring segmentation as {} px is above threshold". format(num_pixels)) # ROI is in (z, y, x) order, so need to transpose or swap x,z axes roi_show = np.transpose(roi_show) surface = pipeline.scalar_field(roi_show) # Contour -> Surface pipeline # create the surface surface = pipeline.contour(surface) # remove many more extraneous points surface = pipeline.user_defined(surface, filter="SmoothPolyDataFilter") surface.filter.number_of_iterations = 400 surface.filter.relaxation_factor = 0.015 # distinguishing pos vs neg curvatures? surface = pipeline.user_defined(surface, filter="Curvatures") surface = self.scene.mlab.pipeline.surface(surface) module_manager = surface.module_manager module_manager.scalar_lut_manager.data_range = np.array([-2, 0]) module_manager.scalar_lut_manager.lut_mode = "gray" ''' # Surface pipleline with contours enabled (similar to above?) surface = pipeline.contour_surface( surface, color=(0.7, 1, 0.7), line_width=6.0) surface.actor.property.representation = 'wireframe' #surface.actor.property.line_width = 6.0 surface.actor.mapper.scalar_visibility = False ''' ''' # IsoSurface pipeline # uses unique IsoSurface module but appears to have # similar output to contour_surface surface = pipeline.iso_surface(surface) # limit contours for simpler surfaces including smaller file sizes; # TODO: consider making settable as arg or through profile surface.contour.number_of_contours = 1 try: # increase min to further reduce complexity surface.contour.minimum_contour = 0.5 surface.contour.maximum_contour = 0.8 except Exception as e: print(e) print("ignoring min/max contour for now") ''' if offset is not None: # translate to offset scaled by isotropic factor surface.actor.actor.position = np.multiply(offset, isotropic)[::-1] # scale surfaces, which expands/contracts but does not appear # to translate the surface position surface.actor.actor.scale = isotropic[::-1] surfaces.append(surface) # keep visual ordering of surfaces when opacity is reduced self.scene.renderer.use_depth_peeling = True print("time to render 3D surface: {}".format(time() - time_start)) self.surfaces = surfaces return surfaces