Skip to content

Viewer

Viewer

This is the function to view the results of the pipeline i.e. the spots found and which genes they were assigned to.

Parameters:

Name Type Description Default
nb Notebook

Notebook containing at least the ref_spots page.

required
background_image Optional[Union[str, np.ndarray]]

Optional file_name or image that will be plotted as the background image. If image, z dimension needs to be first i.e. n_z x n_y x n_x if 3D or n_y x n_x if 2D. If pass 2D image for 3D data, will show same image as background on each z-plane.

'dapi'
gene_marker_file Optional[str]

Path to csv file containing marker and color for each gene. There must be 6 columns in the csv file with the following headers:

  • GeneNames - str, name of gene with first letter capital
  • ColorR - float, Rgb color for plotting
  • ColorG - float, rGb color for plotting
  • ColorB - float, rgB color for plotting
  • napari_symbol - str, symbol used to plot in napari
  • mpl_symbol - str, equivalent of napari symbol in matplotlib.

If it is not provided, then the default file coppafish/plot/results_viewer/legend.gene_color.csv will be used.

None
Source code in coppafish/plot/results_viewer/base.py
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
def __init__(self, nb: Notebook, background_image: Optional[Union[str, np.ndarray]] = 'dapi',
             gene_marker_file: Optional[str] = None):
    """
    This is the function to view the results of the pipeline
    i.e. the spots found and which genes they were assigned to.

    Args:
        nb: Notebook containing at least the `ref_spots` page.
        background_image: Optional file_name or image that will be plotted as the background image.
            If image, z dimension needs to be first i.e. `n_z x n_y x n_x` if 3D or `n_y x n_x` if 2D.
            If pass *2D* image for *3D* data, will show same image as background on each z-plane.
        gene_marker_file: Path to csv file containing marker and color for each gene. There must be 6 columns
            in the csv file with the following headers:

            * GeneNames - str, name of gene with first letter capital
            * ColorR - float, Rgb color for plotting
            * ColorG - float, rGb color for plotting
            * ColorB - float, rgB color for plotting
            * napari_symbol - str, symbol used to plot in napari
            * mpl_symbol - str, equivalent of napari symbol in matplotlib.

            If it is not provided, then the default file *coppafish/plot/results_viewer/legend.gene_color.csv*
            will be used.
    """
    # TODO: flip y axis so origin bottom left
    self.nb = nb
    if gene_marker_file is None:
        gene_marker_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'gene_color.csv')
    gene_legend_info = pd.read_csv(gene_marker_file)

    # indices of genes in notebook to gene_color data - quicker to look up integers than names
    # in change_threshold
    n_legend_genes = len(gene_legend_info['GeneNames'])
    self.legend_gene_symbol = np.asarray(gene_legend_info['mpl_symbol'])
    self.legend_gene_no = np.ones(n_legend_genes, dtype=int) * -1
    for i in range(n_legend_genes):
        # TODO: maybe guard against different cases in this comparison
        gene_ind = np.where(self.nb.call_spots.gene_names == gene_legend_info['GeneNames'][i])[0]
        if len(gene_ind) > 0:
            self.legend_gene_no[i] = gene_ind[0]

    # concatenate anchor and omp spots so can use button to switch between them.
    self.omp_0_ind = self.nb.ref_spots.tile.size  # number of anchor spots
    if self.nb.has_page('omp'):
        self.n_spots = self.omp_0_ind + self.nb.omp.tile.size  # number of anchor + number of omp spots
    else:
        self.n_spots = self.omp_0_ind
    spot_zyx = np.zeros((self.n_spots, 3))
    spot_zyx[:self.omp_0_ind] = (self.nb.ref_spots.local_yxz + self.nb.stitch.tile_origin[self.nb.ref_spots.tile]
                                 )[:, [2, 0, 1]]
    if self.nb.has_page('omp'):
        spot_zyx[self.omp_0_ind:] = (self.nb.omp.local_yxz + self.nb.stitch.tile_origin[self.nb.omp.tile]
                                     )[:, [2, 0, 1]]
    if not self.nb.basic_info.is_3d:
        spot_zyx = spot_zyx[:, 1:]

    # indicate spots shown when plot first opened - omp if exists, else anchor
    if self.nb.has_page('omp'):
        show_spots = np.zeros(self.n_spots, dtype=bool)
        show_spots[self.omp_0_ind:] = quality_threshold(self.nb, 'omp')
    else:
        show_spots = quality_threshold(self.nb, 'anchor')

    # color to plot for all genes in the notebook
    gene_color = np.ones((len(self.nb.call_spots.gene_names), 3))
    for i in range(n_legend_genes):
        if self.legend_gene_no[i] != -1:
            gene_color[self.legend_gene_no[i]] = [gene_legend_info.loc[i, 'ColorR'],
                                                  gene_legend_info.loc[i, 'ColorG'],
                                                  gene_legend_info.loc[i, 'ColorB']]

    self.viewer = napari.Viewer()
    self.viewer.window.qt_viewer.dockLayerList.setVisible(False)
    self.viewer.window.qt_viewer.dockLayerControls.setVisible(False)

    # Add background image if given
    self.diagnostic_layer_ind = 0
    self.image_layer_ind = None
    if background_image is not None:
        if isinstance(background_image, str):
            if background_image.lower() == 'dapi':
                file_name = nb.file_names.big_dapi_image
            elif background_image.lower() == 'anchor':
                file_name = nb.file_names.big_anchor_image
            else:
                file_name = background_image
            if file_name is not None and os.path.isfile(file_name):
                background_image = np.load(file_name)
                if file_name.endswith('.npz'):
                    # Assume image is first array if .npz file
                    background_image = background_image[background_image._files[0]]
            else:
                background_image = None
                warnings.warn(f'No file exists with address =\n{file_name}\nso plotting with no background.')
        if background_image is not None:
            self.viewer.add_image(background_image)
            self.diagnostic_layer_ind = 1
            self.image_layer_ind = 0
            self.viewer.layers[self.image_layer_ind].contrast_limits_range = [background_image.min(),
                                                                              background_image.max()]
            self.image_contrast_slider = QRangeSlider(Qt.Orientation.Horizontal)  # Slider to change score_thresh
            self.image_contrast_slider.setRange(background_image.min(), background_image.max())
            # Make starting lower bound contrast the 95th percentile value so most appears black
            # Use mid_z to quicken up calculation
            mid_z = int(background_image.shape[0]/2)
            start_contrast = np.percentile(background_image[mid_z], [95, 99.99]).astype(int).tolist()
            self.image_contrast_slider.setValue(start_contrast)
            self.change_image_contrast()
            # When dragging, status will show contrast values.
            self.image_contrast_slider.valueChanged.connect(lambda x: self.show_image_contrast(x[0], x[1]))
            # On release of slider, genes shown will change
            self.image_contrast_slider.sliderReleased.connect(self.change_image_contrast)

    # Add legend indicating genes plotted
    self.legend = {'fig': None, 'ax': None}
    self.legend['fig'], self.legend['ax'], n_gene_label_letters = \
        add_legend(gene_legend_info=gene_legend_info, genes=nb.call_spots.gene_names)
    # xy is position of each symbol in legend, need to see which gene clicked on.
    self.legend['xy'] = np.zeros((len(self.legend['ax'].collections), 2), dtype=float)
    self.legend['gene_no'] = np.zeros(len(self.legend['ax'].collections), dtype=int)
    # In legend, each gene name label has at most n_gene_label_letters letters so need to crop
    # gene_names in notebook when looking for corresponding gene in legend.
    gene_names_crop = np.asarray([gene_name[:n_gene_label_letters] for gene_name in nb.call_spots.gene_names])
    for i in range(self.legend['xy'].shape[0]):
        # Position of label for each gene in legend window
        self.legend['xy'][i] = np.asarray(self.legend['ax'].collections[i].get_offsets())
        # gene no in notebook that each position in the legend corresponds to
        self.legend['gene_no'][i] = \
            np.where(gene_names_crop == self.legend['ax'].texts[i].get_text())[0][0]
    self.legend['fig'].mpl_connect('button_press_event', self.update_genes)
    self.viewer.window.add_dock_widget(self.legend['fig'], area='left', name='Genes')
    self.active_genes = np.arange(len(nb.call_spots.gene_names))  # start with all genes shown

    if background_image is not None:
        # Slider to change background image contrast
        self.viewer.window.add_dock_widget(self.image_contrast_slider, area="left", name='Image Contrast')

    # Add all spots in layer as transparent white spots.
    point_size = 10  # with size=4, spots are too small to see
    self.viewer.add_points(spot_zyx, name='Diagnostic', face_color='w', size=point_size + 2, opacity=0,
                           shown=show_spots)

    # Add gene spots with coppafish color code - different layer for each symbol
    if self.nb.has_page('omp'):
        self.spot_gene_no = np.hstack((self.nb.ref_spots.gene_no, self.nb.omp.gene_no))
    else:
        self.spot_gene_no = self.nb.ref_spots.gene_no
    self.label_prefix = 'Gene Symbol:'  # prefix of label for layers showing spots
    for s in np.unique(self.legend_gene_symbol):
        spots_correct_gene = np.isin(self.spot_gene_no, self.legend_gene_no[self.legend_gene_symbol == s])
        if spots_correct_gene.any():
            coords_to_plot = spot_zyx[spots_correct_gene]
            spotcolor_to_plot = gene_color[self.spot_gene_no[spots_correct_gene]]
            symb_to_plot = np.unique(gene_legend_info[self.legend_gene_symbol == s]['napari_symbol'])[0]
            self.viewer.add_points(coords_to_plot, face_color=spotcolor_to_plot, symbol=symb_to_plot,
                                   name=f'{self.label_prefix}{s}', size=point_size,
                                   shown=show_spots[spots_correct_gene])
            # TODO: showing multiple z-planes at once is possible using out_of_slice_display=True,
            #  but at the moment cannot use at same time as show.
            #  When this works, can change n_z shown by changing z-dimension of point_size i.e.
            #  point_size = [z_size, 10, 10] and z_size changes.
            #  On next napari, release should be able to change thickness of spots in z-direction i.e. control what
            #  number of z-planes can be seen at any one time:
            #  https://github.com/napari/napari/issues/4816#issuecomment-1186600574.
            #  Then see find_spots viewer for how I added a z-thick slider

    self.viewer.layers.selection.active = self.viewer.layers[self.diagnostic_layer_ind]
    # so indicates when a spot is selected in viewer status
    # It is needed because layer is transparent so can't see when select spot.
    self.viewer_status_on_select()

    config = self.nb.get_config()['thresholds']
    self.score_omp_multiplier = config['score_omp_multiplier']
    self.score_thresh_slider = QDoubleRangeSlider(Qt.Orientation.Horizontal)  # Slider to change score_thresh
    # Scores for anchor/omp are different so reset score range when change method
    # Max possible score is that found for ref_spots, as this can be more than 1.
    # Max possible omp score is 1.
    max_score = np.around(round_any(nb.ref_spots.score.max(), 0.1, 'ceil'), 2)
    max_score = float(np.clip(max_score, 1, np.inf))
    self.score_range = {'anchor': [config['score_ref'], max_score]}
    if self.nb.has_page('omp'):
        self.score_range['omp'] = [config['score_omp'], max_score]
        self.score_thresh_slider.setValue(self.score_range['omp'])
    else:
        self.score_thresh_slider.setValue(self.score_range['anchor'])
    self.score_thresh_slider.setRange(0, max_score)
    # When dragging, status will show thresh.
    self.score_thresh_slider.valueChanged.connect(lambda x: self.show_score_thresh(x[0], x[1]))
    # On release of slider, genes shown will change
    self.score_thresh_slider.sliderReleased.connect(self.update_plot)
    self.viewer.window.add_dock_widget(self.score_thresh_slider, area="left", name='Score Range')

    # OMP Score Multiplier Slider
    self.omp_score_multiplier_slider = QDoubleSlider(Qt.Orientation.Horizontal)
    self.omp_score_multiplier_slider.setValue(self.score_omp_multiplier)
    self.omp_score_multiplier_slider.setRange(0, 50)
    self.omp_score_multiplier_slider.valueChanged.connect(lambda x: self.show_omp_score_multiplier(x))
    self.omp_score_multiplier_slider.sliderReleased.connect(self.update_plot)

    # intensity is calculated same way for anchor / omp method so do not reset intensity threshold
    # when change method.
    self.intensity_thresh_slider = QDoubleSlider(Qt.Orientation.Horizontal)
    self.intensity_thresh_slider.setRange(0, 1)
    intensity_thresh = get_intensity_thresh(nb)
    self.intensity_thresh_slider.setValue(intensity_thresh)
    # When dragging, status will show thresh.
    self.intensity_thresh_slider.valueChanged.connect(lambda x: self.show_intensity_thresh(x))
    # On release of slider, genes shown will change
    self.intensity_thresh_slider.sliderReleased.connect(self.update_plot)
    self.viewer.window.add_dock_widget(self.intensity_thresh_slider, area="left", name='Intensity Threshold')

    if self.nb.has_page('omp'):
        self.method_buttons = ButtonMethodWindow('OMP')  # Buttons to change between Anchor and OMP spots showing.
    else:
        self.method_buttons = ButtonMethodWindow('Anchor')
    self.method_buttons.button_anchor.clicked.connect(self.button_anchor_clicked)
    self.method_buttons.button_omp.clicked.connect(self.button_omp_clicked)
    if self.nb.has_page('omp'):
        self.viewer.window.add_dock_widget(self.omp_score_multiplier_slider, area="left",
                                           name='OMP Score Multiplier')
        # Only have button to change method if have omp page too.
        self.viewer.window.add_dock_widget(self.method_buttons, area="left", name='Method')

    self.key_call_functions()
    if self.nb.basic_info.is_3d:
        self.viewer.dims.axis_labels = ['z', 'y', 'x']
    else:
        self.viewer.dims.axis_labels = ['y', 'x']

    napari.run()