celldetective 1.5.0b1__py3-none-any.whl → 1.5.0b3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. celldetective/_version.py +1 -1
  2. celldetective/gui/InitWindow.py +51 -12
  3. celldetective/gui/base/components.py +22 -1
  4. celldetective/gui/base_annotator.py +20 -9
  5. celldetective/gui/control_panel.py +21 -16
  6. celldetective/gui/event_annotator.py +51 -1060
  7. celldetective/gui/gui_utils.py +14 -5
  8. celldetective/gui/interactions_block.py +55 -25
  9. celldetective/gui/interactive_timeseries_viewer.py +11 -1
  10. celldetective/gui/measure_annotator.py +1064 -0
  11. celldetective/gui/plot_measurements.py +2 -4
  12. celldetective/gui/plot_signals_ui.py +3 -4
  13. celldetective/gui/process_block.py +298 -72
  14. celldetective/gui/viewers/base_viewer.py +134 -3
  15. celldetective/gui/viewers/contour_viewer.py +4 -4
  16. celldetective/gui/workers.py +25 -10
  17. celldetective/measure.py +3 -0
  18. celldetective/napari/utils.py +29 -19
  19. celldetective/processes/load_table.py +55 -0
  20. celldetective/processes/measure_cells.py +107 -81
  21. celldetective/processes/track_cells.py +39 -39
  22. celldetective/segmentation.py +1 -1
  23. celldetective/tracking.py +9 -0
  24. celldetective/utils/data_loaders.py +21 -1
  25. celldetective/utils/image_loaders.py +3 -0
  26. celldetective/utils/masks.py +1 -1
  27. celldetective/utils/maths.py +14 -1
  28. {celldetective-1.5.0b1.dist-info → celldetective-1.5.0b3.dist-info}/METADATA +1 -1
  29. {celldetective-1.5.0b1.dist-info → celldetective-1.5.0b3.dist-info}/RECORD +35 -32
  30. tests/gui/test_enhancements.py +351 -0
  31. tests/test_notebooks.py +2 -1
  32. {celldetective-1.5.0b1.dist-info → celldetective-1.5.0b3.dist-info}/WHEEL +0 -0
  33. {celldetective-1.5.0b1.dist-info → celldetective-1.5.0b3.dist-info}/entry_points.txt +0 -0
  34. {celldetective-1.5.0b1.dist-info → celldetective-1.5.0b3.dist-info}/licenses/LICENSE +0 -0
  35. {celldetective-1.5.0b1.dist-info → celldetective-1.5.0b3.dist-info}/top_level.txt +0 -0
@@ -11,7 +11,10 @@ from PyQt5.QtWidgets import (
11
11
  QShortcut,
12
12
  QLineEdit,
13
13
  QSlider,
14
+ QAction,
15
+ QMenu,
14
16
  )
17
+ from celldetective.gui.interactive_timeseries_viewer import InteractiveEventViewer
15
18
  from PyQt5.QtCore import Qt, QSize, QThread, pyqtSignal, QTimer
16
19
  from PyQt5.QtGui import QKeySequence, QIntValidator
17
20
 
@@ -266,66 +269,65 @@ class EventAnnotator(BaseAnnotator):
266
269
 
267
270
  QApplication.processEvents()
268
271
 
269
- def write_new_event_class(self):
272
+ # Add Menu for Interactive Plotter
273
+ menubar = self.menuBar()
274
+ viewMenu = menubar.addMenu("View")
270
275
 
271
- if self.class_name_le.text() == "":
272
- self.target_class = "class"
273
- self.target_time = "t0"
274
- else:
275
- self.target_class = "class_" + self.class_name_le.text()
276
- self.target_time = "t_" + self.class_name_le.text()
277
-
278
- if self.target_class in list(self.df_tracks.columns):
276
+ openPlotterAct = QAction("Interactive Plotter", self)
277
+ openPlotterAct.setShortcut("Ctrl+P")
278
+ openPlotterAct.setStatusTip("Open interactive signal plotter for corrections")
279
+ openPlotterAct.triggered.connect(self.launch_interactive_viewer)
280
+ viewMenu.addAction(openPlotterAct)
279
281
 
280
- msgBox = QMessageBox()
281
- msgBox.setIcon(QMessageBox.Warning)
282
- msgBox.setText(
283
- "This event name already exists. If you proceed,\nall annotated data will be rewritten. Do you wish to continue?"
282
+ def launch_interactive_viewer(self):
283
+ if (
284
+ not hasattr(self, "plotter")
285
+ or self.plotter is None
286
+ or not self.plotter.isVisible()
287
+ ):
288
+ label = None
289
+ if hasattr(self, "class_name") and self.class_name.startswith("class_"):
290
+ label = self.class_name.replace("class_", "")
291
+
292
+ # Create with shared DF and callback
293
+ self.plotter = InteractiveEventViewer(
294
+ self.trajectories_path,
295
+ df=self.df_tracks,
296
+ event_label=label,
297
+ callback=self.on_viewer_update,
298
+ parent=self,
284
299
  )
285
- msgBox.setWindowTitle("Warning")
286
- msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
287
- returnValue = msgBox.exec()
288
- if returnValue == QMessageBox.No:
289
- return None
290
- else:
291
- pass
292
-
293
- fill_option = np.where([c.isChecked() for c in self.class_option_rb])[0][0]
294
- self.df_tracks.loc[:, self.target_class] = fill_option
295
- if fill_option == 0:
296
- self.df_tracks.loc[:, self.target_time] = 0.1
297
- else:
298
- self.df_tracks.loc[:, self.target_time] = -1
300
+ self.plotter.show()
301
+ self.plotter.activateWindow()
299
302
 
300
- self.class_choice_cb.clear()
301
- cols = np.array(self.df_tracks.columns)
302
- self.class_cols = np.array(
303
- [c.startswith("class") for c in list(self.df_tracks.columns)]
304
- )
305
- self.class_cols = list(cols[self.class_cols])
306
- if "class_id" in self.class_cols:
307
- self.class_cols.remove("class_id")
308
- if "class_color" in self.class_cols:
309
- self.class_cols.remove("class_color")
310
- self.class_choice_cb.addItems(self.class_cols)
311
- idx = self.class_choice_cb.findText(self.target_class)
312
- self.class_choice_cb.setCurrentIndex(idx)
313
-
314
- self.newClassWidget.close()
303
+ def on_viewer_update(self):
304
+ """Callback from interactive viewer to refresh annotator."""
305
+ self.compute_status_and_colors(0)
306
+ self.update_scatters_only()
307
+ self.fcanvas.canvas.draw_idle()
315
308
 
316
- # def close_without_new_class(self):
317
- # self.newClassWidget.close()
309
+ def update_scatters_only(self):
310
+ """Update only the scatters without reloading the image."""
311
+ self.status_scatter.set_offsets(self.positions[self.framedata])
312
+ self.status_scatter.set_color(self.colors[self.framedata][:, 1])
313
+ self.class_scatter.set_offsets(self.positions[self.framedata])
314
+ self.class_scatter.set_edgecolor(self.colors[self.framedata][:, 0])
318
315
 
319
316
  def compute_status_and_colors(self, i):
320
317
 
321
318
  self.class_name = self.class_choice_cb.currentText()
322
- self.expected_status = "status"
319
+ if self.class_name == "":
320
+ self.class_name = "class"
321
+
323
322
  suffix = self.class_name.replace("class", "").replace("_", "", 1)
324
323
  if suffix != "":
325
- self.expected_status += "_" + suffix
324
+ self.expected_status = "status_" + suffix
326
325
  self.expected_time = "t_" + suffix
327
326
  else:
327
+ self.expected_status = "status"
328
328
  self.expected_time = "t0"
329
+ self.expected_class = self.class_name
330
+
329
331
  self.time_name = self.expected_time
330
332
  self.status_name = self.expected_status
331
333
 
@@ -371,6 +373,9 @@ class EventAnnotator(BaseAnnotator):
371
373
 
372
374
  self.fcanvas.canvas.draw()
373
375
 
376
+ # def close_without_new_class(self):
377
+ # self.newClassWidget.close()
378
+
374
379
  def cancel_selection(self):
375
380
 
376
381
  super().cancel_selection()
@@ -1030,1017 +1035,3 @@ class EventAnnotator(BaseAnnotator):
1030
1035
  self.start_btn.setShortcut(QKeySequence("f"))
1031
1036
 
1032
1037
 
1033
- class MeasureAnnotator(BaseAnnotator):
1034
-
1035
- def __init__(self, *args, **kwargs):
1036
-
1037
- super().__init__(read_config=False, *args, **kwargs)
1038
-
1039
- self.setWindowTitle("Static annotator")
1040
-
1041
- self.int_validator = QIntValidator()
1042
- self.current_alpha = 0.5
1043
- self.value_magnitude = 1
1044
-
1045
- epsilon = 0.01
1046
- self.observed_min_intensity = 0
1047
- self.observed_max_intensity = 0 + epsilon
1048
-
1049
- self.current_frame = 0
1050
- self.show_fliers = False
1051
- self.status_name = "group"
1052
-
1053
- if self.proceed:
1054
-
1055
- self.labels = locate_labels(self.pos, population=self.mode)
1056
-
1057
- self.current_channel = 0
1058
- self.frame_lbl = QLabel("position: ")
1059
- self.static_image()
1060
-
1061
- self.populate_window()
1062
- self.changed_class()
1063
-
1064
- self.previous_index = None
1065
- if hasattr(self, "contrast_slider"):
1066
- self.im.set_clim(
1067
- self.contrast_slider.value()[0], self.contrast_slider.value()[1]
1068
- )
1069
-
1070
- else:
1071
- self.close()
1072
-
1073
- def locate_tracks(self):
1074
- """
1075
- Locate the tracks.
1076
- """
1077
-
1078
- if not os.path.exists(self.trajectories_path):
1079
-
1080
- msgBox = QMessageBox()
1081
- msgBox.setIcon(QMessageBox.Warning)
1082
- msgBox.setText("The trajectories cannot be detected.")
1083
- msgBox.setWindowTitle("Warning")
1084
- msgBox.setStandardButtons(QMessageBox.Ok)
1085
- returnValue = msgBox.exec()
1086
- if returnValue == QMessageBox.Yes:
1087
- self.close()
1088
- else:
1089
-
1090
- # Load and prep tracks
1091
- self.df_tracks = pd.read_csv(self.trajectories_path)
1092
- if "TRACK_ID" in self.df_tracks.columns:
1093
- self.df_tracks = self.df_tracks.sort_values(by=["TRACK_ID", "FRAME"])
1094
- else:
1095
- self.df_tracks = self.df_tracks.sort_values(by=["ID", "FRAME"])
1096
-
1097
- cols = np.array(self.df_tracks.columns)
1098
- self.class_cols = np.array(
1099
- [
1100
- c.startswith("group") or c.startswith("class")
1101
- for c in list(self.df_tracks.columns)
1102
- ]
1103
- )
1104
- self.class_cols = list(cols[self.class_cols])
1105
-
1106
- to_remove = ["class_id", "group_color", "class_color"]
1107
- for col in to_remove:
1108
- try:
1109
- self.class_cols.remove(col)
1110
- except:
1111
- pass
1112
-
1113
- if len(self.class_cols) > 0:
1114
- self.status_name = self.class_cols[0]
1115
- else:
1116
- self.status_name = "group"
1117
-
1118
- if self.status_name not in self.df_tracks.columns:
1119
- # only create the status column if it does not exist to not erase static classification results
1120
- self.make_status_column()
1121
- else:
1122
- # all good, do nothing
1123
- pass
1124
-
1125
- all_states = self.df_tracks.loc[:, self.status_name].tolist()
1126
- all_states = np.array(all_states)
1127
- self.state_color_map = color_from_state(all_states, recently_modified=False)
1128
- self.df_tracks["group_color"] = self.df_tracks[self.status_name].apply(
1129
- self.assign_color_state
1130
- )
1131
-
1132
- self.df_tracks = self.df_tracks.dropna(subset=["POSITION_X", "POSITION_Y"])
1133
- self.df_tracks["x_anim"] = self.df_tracks["POSITION_X"]
1134
- self.df_tracks["y_anim"] = self.df_tracks["POSITION_Y"]
1135
- self.df_tracks["x_anim"] = self.df_tracks["x_anim"].astype(int)
1136
- self.df_tracks["y_anim"] = self.df_tracks["y_anim"].astype(int)
1137
-
1138
- self.extract_scatter_from_trajectories()
1139
- if "TRACK_ID" in self.df_tracks.columns:
1140
- self.track_of_interest = self.df_tracks.dropna(subset="TRACK_ID")[
1141
- "TRACK_ID"
1142
- ].min()
1143
- else:
1144
- self.track_of_interest = self.df_tracks.dropna(subset="ID")["ID"].min()
1145
-
1146
- self.loc_t = []
1147
- self.loc_idx = []
1148
- for t in range(len(self.tracks)):
1149
- indices = np.where(self.tracks[t] == self.track_of_interest)[0]
1150
- if len(indices) > 0:
1151
- self.loc_t.append(t)
1152
- self.loc_idx.append(indices[0])
1153
-
1154
- self.MinMaxScaler = MinMaxScaler()
1155
- self.columns_to_rescale = list(self.df_tracks.columns)
1156
-
1157
- cols_to_remove = [
1158
- "group",
1159
- "group_color",
1160
- "status",
1161
- "status_color",
1162
- "class_color",
1163
- "TRACK_ID",
1164
- "FRAME",
1165
- "x_anim",
1166
- "y_anim",
1167
- "t",
1168
- "dummy",
1169
- "group_color",
1170
- "state",
1171
- "generation",
1172
- "root",
1173
- "parent",
1174
- "class_id",
1175
- "class",
1176
- "t0",
1177
- "POSITION_X",
1178
- "POSITION_Y",
1179
- "position",
1180
- "well",
1181
- "well_index",
1182
- "well_name",
1183
- "pos_name",
1184
- "index",
1185
- "concentration",
1186
- "cell_type",
1187
- "antibody",
1188
- "pharmaceutical_agent",
1189
- "ID",
1190
- ] + self.class_cols
1191
-
1192
- meta = get_experiment_metadata(self.exp_dir)
1193
- if meta is not None:
1194
- keys = list(meta.keys())
1195
- cols_to_remove.extend(keys)
1196
-
1197
- labels = get_experiment_labels(self.exp_dir)
1198
- if labels is not None:
1199
- keys = list(labels.keys())
1200
- cols_to_remove.extend(labels)
1201
-
1202
- for tr in cols_to_remove:
1203
- try:
1204
- self.columns_to_rescale.remove(tr)
1205
- except:
1206
- pass
1207
-
1208
- x = self.df_tracks[self.columns_to_rescale].values
1209
- self.MinMaxScaler.fit(x)
1210
-
1211
- def populate_options_layout(self):
1212
- # clear options hbox
1213
- for i in reversed(range(self.options_hbox.count())):
1214
- self.options_hbox.itemAt(i).widget().setParent(None)
1215
-
1216
- time_option_hbox = QHBoxLayout()
1217
- time_option_hbox.setContentsMargins(100, 0, 100, 0)
1218
- time_option_hbox.setSpacing(0)
1219
-
1220
- self.time_of_interest_label = QLabel("phenotype: ")
1221
- time_option_hbox.addWidget(self.time_of_interest_label, 30)
1222
-
1223
- self.time_of_interest_le = QLineEdit()
1224
- self.time_of_interest_le.setValidator(self.int_validator)
1225
- time_option_hbox.addWidget(self.time_of_interest_le)
1226
-
1227
- self.suppr_btn = QPushButton("")
1228
- self.suppr_btn.setStyleSheet(self.button_select_all)
1229
- self.suppr_btn.setIcon(icon(MDI6.delete, color="black"))
1230
- self.suppr_btn.setToolTip("Delete cell")
1231
- self.suppr_btn.setIconSize(QSize(20, 20))
1232
- self.suppr_btn.clicked.connect(self.del_cell)
1233
- time_option_hbox.addWidget(self.suppr_btn)
1234
-
1235
- self.options_hbox.addLayout(time_option_hbox)
1236
-
1237
- def update_widgets(self):
1238
-
1239
- self.class_label.setText("characteristic \n group: ")
1240
- self.update_class_cb()
1241
- self.add_class_btn.setToolTip("Add a new characteristic group")
1242
- self.del_class_btn.setToolTip("Delete a characteristic group")
1243
-
1244
- self.export_btn.disconnect()
1245
- self.export_btn.clicked.connect(self.export_measurements)
1246
-
1247
- def update_class_cb(self):
1248
-
1249
- self.class_choice_cb.disconnect()
1250
- self.class_choice_cb.clear()
1251
- cols = np.array(self.df_tracks.columns)
1252
- self.class_cols = np.array(
1253
- [
1254
- c.startswith("group") or c.startswith("status")
1255
- for c in list(self.df_tracks.columns)
1256
- ]
1257
- )
1258
- self.class_cols = list(cols[self.class_cols])
1259
-
1260
- to_remove = [
1261
- "group_id",
1262
- "group_color",
1263
- "class_id",
1264
- "class_color",
1265
- "status_color",
1266
- ]
1267
- for col in to_remove:
1268
- try:
1269
- self.class_cols.remove(col)
1270
- except Exception:
1271
- pass
1272
-
1273
- self.class_choice_cb.addItems(self.class_cols)
1274
- self.class_choice_cb.currentIndexChanged.connect(self.changed_class)
1275
-
1276
- def populate_window(self):
1277
-
1278
- super().populate_window()
1279
- # Left panel updates
1280
- self.populate_options_layout()
1281
- self.update_widgets()
1282
-
1283
- self.annotation_btns_to_hide = [
1284
- self.time_of_interest_label,
1285
- self.time_of_interest_le,
1286
- self.suppr_btn,
1287
- ]
1288
- self.hide_annotation_buttons()
1289
-
1290
- # Right panel
1291
- animation_buttons_box = QHBoxLayout()
1292
- animation_buttons_box.addWidget(self.frame_lbl, 20, alignment=Qt.AlignLeft)
1293
-
1294
- self.first_frame_btn = QPushButton()
1295
- self.first_frame_btn.clicked.connect(self.set_previous_frame)
1296
- self.first_frame_btn.setShortcut(QKeySequence("f"))
1297
- self.first_frame_btn.setIcon(icon(MDI6.page_first, color="black"))
1298
- self.first_frame_btn.setStyleSheet(self.button_select_all)
1299
- self.first_frame_btn.setFixedSize(QSize(60, 60))
1300
- self.first_frame_btn.setIconSize(QSize(30, 30))
1301
-
1302
- self.last_frame_btn = QPushButton()
1303
- self.last_frame_btn.clicked.connect(self.set_next_frame)
1304
- self.last_frame_btn.setShortcut(QKeySequence("l"))
1305
- self.last_frame_btn.setIcon(icon(MDI6.page_last, color="black"))
1306
- self.last_frame_btn.setStyleSheet(self.button_select_all)
1307
- self.last_frame_btn.setFixedSize(QSize(60, 60))
1308
- self.last_frame_btn.setIconSize(QSize(30, 30))
1309
-
1310
- self.frame_slider = QSlider(Qt.Horizontal)
1311
- self.frame_slider.setFixedSize(200, 30)
1312
- self.frame_slider.setRange(0, self.len_movie - 1)
1313
- self.frame_slider.setValue(0)
1314
- self.frame_slider.valueChanged.connect(self.update_frame)
1315
-
1316
- self.start_btn = QPushButton()
1317
- self.start_btn.clicked.connect(self.start)
1318
- self.start_btn.setIcon(icon(MDI6.play, color="black"))
1319
- self.start_btn.setFixedSize(QSize(60, 60))
1320
- self.start_btn.setStyleSheet(self.button_select_all)
1321
- self.start_btn.setIconSize(QSize(30, 30))
1322
- self.start_btn.hide()
1323
-
1324
- animation_buttons_box.addWidget(
1325
- self.first_frame_btn, 5, alignment=Qt.AlignRight
1326
- )
1327
- animation_buttons_box.addWidget(self.frame_slider, 5, alignment=Qt.AlignCenter)
1328
- animation_buttons_box.addWidget(self.last_frame_btn, 5, alignment=Qt.AlignLeft)
1329
-
1330
- self.right_panel.addLayout(animation_buttons_box, 5)
1331
-
1332
- self.right_panel.addWidget(self.fcanvas, 90)
1333
-
1334
- contrast_hbox = QHBoxLayout()
1335
- contrast_hbox.setContentsMargins(150, 5, 150, 5)
1336
- self.contrast_slider = QLabeledDoubleRangeSlider()
1337
-
1338
- self.contrast_slider.setSingleStep(0.001)
1339
- self.contrast_slider.setTickInterval(0.001)
1340
- self.contrast_slider.setOrientation(Qt.Horizontal)
1341
- self.contrast_slider.setRange(
1342
- *[np.nanpercentile(self.img, 0.001), np.nanpercentile(self.img, 99.999)]
1343
- )
1344
- self.contrast_slider.setValue(
1345
- [np.nanpercentile(self.img, 1), np.nanpercentile(self.img, 99.99)]
1346
- )
1347
- self.contrast_slider.valueChanged.connect(self.contrast_slider_action)
1348
- contrast_hbox.addWidget(QLabel("contrast: "))
1349
- contrast_hbox.addWidget(self.contrast_slider, 90)
1350
- self.right_panel.addLayout(contrast_hbox, 5)
1351
- self.alpha_slider = QLabeledDoubleSlider()
1352
- self.alpha_slider.setSingleStep(0.001)
1353
- self.alpha_slider.setOrientation(Qt.Horizontal)
1354
- self.alpha_slider.setRange(0, 1)
1355
- self.alpha_slider.setValue(self.current_alpha)
1356
- self.alpha_slider.setDecimals(3)
1357
- self.alpha_slider.valueChanged.connect(self.set_transparency)
1358
-
1359
- slider_alpha_hbox = QHBoxLayout()
1360
- slider_alpha_hbox.setContentsMargins(150, 5, 150, 5)
1361
- slider_alpha_hbox.addWidget(QLabel("transparency: "), 10)
1362
- slider_alpha_hbox.addWidget(self.alpha_slider, 90)
1363
- self.right_panel.addLayout(slider_alpha_hbox)
1364
-
1365
- channel_hbox = QHBoxLayout()
1366
- self.choose_channel = QComboBox()
1367
- self.choose_channel.addItems(self.channel_names)
1368
- self.choose_channel.currentIndexChanged.connect(self.changed_channel)
1369
- channel_hbox.addWidget(self.choose_channel)
1370
- self.right_panel.addLayout(channel_hbox, 5)
1371
-
1372
- self.draw_frame(0)
1373
- self.vmin = self.contrast_slider.value()[0]
1374
- self.vmax = self.contrast_slider.value()[1]
1375
- self.im.set_clim(vmin=self.vmin, vmax=self.vmax)
1376
-
1377
- self.fcanvas.canvas.draw()
1378
- self.plot_signals()
1379
-
1380
- def static_image(self):
1381
- """
1382
- Load an image.
1383
-
1384
- """
1385
-
1386
- self.framedata = 0
1387
- self.current_label = self.labels[self.current_frame]
1388
- self.fig, self.ax = plt.subplots(tight_layout=True)
1389
- self.fcanvas = FigureCanvas(self.fig, interactive=True)
1390
- self.ax.clear()
1391
- # print(self.current_stack.shape)
1392
- self.im = self.ax.imshow(self.img, cmap="gray")
1393
- self.status_scatter = self.ax.scatter(
1394
- self.positions[0][:, 0],
1395
- self.positions[0][:, 1],
1396
- marker="o",
1397
- facecolors="none",
1398
- edgecolors=self.colors[0][:, 0],
1399
- s=200,
1400
- picker=True,
1401
- )
1402
- self.im_mask = self.ax.imshow(
1403
- np.ma.masked_where(self.current_label == 0, self.current_label),
1404
- cmap="viridis",
1405
- interpolation="none",
1406
- alpha=self.current_alpha,
1407
- vmin=0,
1408
- vmax=np.nanmax(self.labels.flatten()),
1409
- )
1410
- self.ax.set_xticks([])
1411
- self.ax.set_yticks([])
1412
- self.ax.set_aspect("equal")
1413
-
1414
- self.fig.set_facecolor("none") # or 'None'
1415
- self.fig.canvas.setStyleSheet("background-color: black;")
1416
-
1417
- self.fig.canvas.mpl_connect("pick_event", self.on_scatter_pick)
1418
- self.fcanvas.canvas.draw()
1419
-
1420
- def plot_signals(self):
1421
-
1422
- # try:
1423
- current_frame = (
1424
- self.current_frame
1425
- ) # Assuming you have a variable for the current frame
1426
-
1427
- yvalues = []
1428
- all_yvalues = []
1429
- current_yvalues = []
1430
- labels = []
1431
- range_values = []
1432
-
1433
- for i in range(len(self.signal_choice_cb)):
1434
-
1435
- signal_choice = self.signal_choice_cb[i].currentText()
1436
-
1437
- if signal_choice != "--":
1438
- if "TRACK_ID" in self.df_tracks.columns:
1439
- ydata = self.df_tracks.loc[
1440
- (self.df_tracks["TRACK_ID"] == self.track_of_interest)
1441
- & (self.df_tracks["FRAME"] == current_frame),
1442
- signal_choice,
1443
- ].to_numpy()
1444
- else:
1445
- ydata = self.df_tracks.loc[
1446
- (self.df_tracks["ID"] == self.track_of_interest), signal_choice
1447
- ].to_numpy()
1448
- all_ydata = self.df_tracks.loc[:, signal_choice].to_numpy()
1449
- ydataNaN = ydata
1450
- ydata = ydata[ydata == ydata] # remove nan
1451
-
1452
- current_ydata = self.df_tracks.loc[
1453
- (self.df_tracks["FRAME"] == current_frame), signal_choice
1454
- ].to_numpy()
1455
- current_ydata = current_ydata[current_ydata == current_ydata]
1456
- all_ydata = all_ydata[all_ydata == all_ydata]
1457
- yvalues.extend(ydataNaN)
1458
- current_yvalues.append(current_ydata)
1459
- all_yvalues.append(all_ydata)
1460
- range_values.extend(all_ydata)
1461
- labels.append(signal_choice)
1462
-
1463
- self.cell_ax.clear()
1464
- if self.log_scale:
1465
- self.cell_ax.set_yscale("log")
1466
- else:
1467
- self.cell_ax.set_yscale("linear")
1468
-
1469
- if len(yvalues) > 0:
1470
- try:
1471
- self.cell_ax.boxplot(all_yvalues, showfliers=self.show_fliers)
1472
- except Exception as e:
1473
- print(f"{e=}")
1474
-
1475
- x_pos = np.arange(len(all_yvalues)) + 1
1476
- for index, feature in enumerate(current_yvalues):
1477
- x_values_strip = (index + 1) + np.random.normal(
1478
- 0, 0.04, size=len(feature)
1479
- )
1480
- self.cell_ax.plot(
1481
- x_values_strip,
1482
- feature,
1483
- marker="o",
1484
- linestyle="None",
1485
- color=tab10.colors[0],
1486
- alpha=0.1,
1487
- )
1488
- self.cell_ax.plot(
1489
- x_pos,
1490
- yvalues,
1491
- marker="H",
1492
- linestyle="None",
1493
- color=tab10.colors[3],
1494
- alpha=1,
1495
- )
1496
- range_values = np.array(range_values)
1497
- range_values = range_values[range_values == range_values]
1498
-
1499
- if len(range_values[range_values > 0]) > 0:
1500
- self.value_magnitude = np.nanmin(
1501
- range_values[range_values > 0]
1502
- ) - 0.03 * (
1503
- np.nanmax(range_values[range_values > 0])
1504
- - np.nanmin(range_values[range_values > 0])
1505
- )
1506
- else:
1507
- self.value_magnitude = 1
1508
-
1509
- self.non_log_ymin = np.nanmin(range_values) - 0.03 * (
1510
- np.nanmax(range_values) - np.nanmin(range_values)
1511
- )
1512
- self.non_log_ymax = np.nanmax(range_values) + 0.03 * (
1513
- np.nanmax(range_values) - np.nanmin(range_values)
1514
- )
1515
- if self.cell_ax.get_yscale() == "linear":
1516
- self.cell_ax.set_ylim(self.non_log_ymin, self.non_log_ymax)
1517
- else:
1518
- self.cell_ax.set_ylim(self.value_magnitude, self.non_log_ymax)
1519
- else:
1520
- self.cell_ax.text(
1521
- 0.5,
1522
- 0.5,
1523
- "No data available",
1524
- horizontalalignment="center",
1525
- verticalalignment="center",
1526
- transform=self.cell_ax.transAxes,
1527
- )
1528
-
1529
- self.cell_fcanvas.canvas.draw()
1530
-
1531
- def plot_red_points(self, ax):
1532
- yvalues = []
1533
- current_frame = self.current_frame
1534
- for i in range(len(self.signal_choice_cb)):
1535
- signal_choice = self.signal_choice_cb[i].currentText()
1536
- if signal_choice != "--":
1537
- # print(f'plot signal {signal_choice} for cell {self.track_of_interest} at frame {current_frame}')
1538
- if "TRACK_ID" in self.df_tracks.columns:
1539
- ydata = self.df_tracks.loc[
1540
- (self.df_tracks["TRACK_ID"] == self.track_of_interest)
1541
- & (self.df_tracks["FRAME"] == current_frame),
1542
- signal_choice,
1543
- ].to_numpy()
1544
- else:
1545
- ydata = self.df_tracks.loc[
1546
- (self.df_tracks["ID"] == self.track_of_interest)
1547
- & (self.df_tracks["FRAME"] == current_frame),
1548
- signal_choice,
1549
- ].to_numpy()
1550
- ydata = ydata[ydata == ydata] # remove nan
1551
- yvalues.extend(ydata)
1552
- x_pos = np.arange(len(yvalues)) + 1
1553
- ax.plot(
1554
- x_pos, yvalues, marker="H", linestyle="None", color=tab10.colors[3], alpha=1
1555
- ) # Plot red points representing cells
1556
- self.cell_fcanvas.canvas.draw()
1557
-
1558
- def select_single_cell(self, index, timepoint):
1559
-
1560
- self.correct_btn.setEnabled(True)
1561
- self.cancel_btn.setEnabled(True)
1562
- self.del_shortcut.setEnabled(True)
1563
-
1564
- self.track_of_interest = self.tracks[timepoint][index]
1565
- print(f"You selected cell #{self.track_of_interest}...")
1566
- self.give_cell_information()
1567
-
1568
- if len(self.cell_ax.lines) > 0:
1569
- self.cell_ax.lines[
1570
- -1
1571
- ].remove() # Remove the last line (red points) from the plot
1572
- self.plot_red_points(self.cell_ax)
1573
- else:
1574
- self.plot_signals()
1575
-
1576
- self.loc_t = []
1577
- self.loc_idx = []
1578
- for t in range(len(self.tracks)):
1579
- indices = np.where(self.tracks[t] == self.track_of_interest)[0]
1580
- if len(indices) > 0:
1581
- self.loc_t.append(t)
1582
- self.loc_idx.append(indices[0])
1583
-
1584
- self.previous_color = []
1585
- for t, idx in zip(self.loc_t, self.loc_idx):
1586
- self.previous_color.append(self.colors[t][idx].copy())
1587
- self.colors[t][idx] = "lime"
1588
-
1589
- self.draw_frame(self.current_frame)
1590
- self.fcanvas.canvas.draw()
1591
-
1592
- def cancel_selection(self):
1593
- super().cancel_selection()
1594
- self.event = None
1595
- self.draw_frame(self.current_frame)
1596
- self.fcanvas.canvas.draw()
1597
-
1598
- def export_measurements(self):
1599
-
1600
- auto_dataset_name = (
1601
- self.pos.split(os.sep)[-4]
1602
- + "_"
1603
- + self.pos.split(os.sep)[-2]
1604
- + f"_{str(self.current_frame).zfill(3)}"
1605
- + f"_{self.status_name}.npy"
1606
- )
1607
-
1608
- if self.normalized_signals:
1609
- self.normalize_features_btn.click()
1610
-
1611
- subdf = self.df_tracks.loc[self.df_tracks["FRAME"] == self.current_frame, :]
1612
- subdf["class"] = subdf[self.status_name]
1613
- dico = subdf.to_dict("records")
1614
-
1615
- pathsave = QFileDialog.getSaveFileName(
1616
- self, "Select file name", self.exp_dir + auto_dataset_name, ".npy"
1617
- )[0]
1618
- if pathsave != "":
1619
- if not pathsave.endswith(".npy"):
1620
- pathsave += ".npy"
1621
- try:
1622
- np.save(pathsave, dico)
1623
- print(f"File successfully written in {pathsave}.")
1624
- except Exception as e:
1625
- print(f"Error {e}...")
1626
-
1627
- def set_next_frame(self):
1628
-
1629
- self.current_frame = self.current_frame + 1
1630
- if self.current_frame > self.len_movie - 1:
1631
- self.current_frame == self.len_movie - 1
1632
- self.frame_slider.setValue(self.current_frame)
1633
- self.update_frame()
1634
- self.start_btn.setShortcut(QKeySequence("f"))
1635
-
1636
- def set_previous_frame(self):
1637
-
1638
- self.current_frame = self.current_frame - 1
1639
- if self.current_frame < 0:
1640
- self.current_frame == 0
1641
- self.frame_slider.setValue(self.current_frame)
1642
- self.update_frame()
1643
-
1644
- self.start_btn.setShortcut(QKeySequence("l"))
1645
-
1646
- def write_new_event_class(self):
1647
-
1648
- if self.class_name_le.text() == "":
1649
- self.target_class = "group"
1650
- else:
1651
- self.target_class = "group_" + self.class_name_le.text()
1652
-
1653
- if self.target_class in list(self.df_tracks.columns):
1654
- msgBox = QMessageBox()
1655
- msgBox.setIcon(QMessageBox.Warning)
1656
- msgBox.setText(
1657
- "This characteristic group name already exists. If you proceed,\nall annotated data will be rewritten. Do you wish to continue?"
1658
- )
1659
- msgBox.setWindowTitle("Warning")
1660
- msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
1661
- returnValue = msgBox.exec()
1662
- if returnValue == QMessageBox.No:
1663
- return None
1664
- else:
1665
- pass
1666
-
1667
- self.df_tracks.loc[:, self.target_class] = 0
1668
-
1669
- self.update_class_cb()
1670
-
1671
- idx = self.class_choice_cb.findText(self.target_class)
1672
- self.status_name = self.target_class
1673
- self.class_choice_cb.setCurrentIndex(idx)
1674
- self.newClassWidget.close()
1675
-
1676
- def hide_annotation_buttons(self):
1677
-
1678
- for a in self.annotation_btns_to_hide:
1679
- a.hide()
1680
- self.time_of_interest_label.setEnabled(False)
1681
- self.time_of_interest_le.setText("")
1682
- self.time_of_interest_le.setEnabled(False)
1683
-
1684
- def set_transparency(self):
1685
- self.current_alpha = self.alpha_slider.value()
1686
- self.im_mask.set_alpha(self.current_alpha)
1687
- self.fcanvas.canvas.draw()
1688
-
1689
- def show_annotation_buttons(self):
1690
-
1691
- for a in self.annotation_btns_to_hide:
1692
- a.show()
1693
-
1694
- self.time_of_interest_label.setEnabled(True)
1695
- self.time_of_interest_le.setEnabled(True)
1696
- self.correct_btn.setText("submit")
1697
-
1698
- self.correct_btn.disconnect()
1699
- self.correct_btn.clicked.connect(self.apply_modification)
1700
-
1701
- def give_cell_information(self):
1702
-
1703
- try:
1704
- cell_selected = f"cell: {self.track_of_interest}\n"
1705
- if "TRACK_ID" in self.df_tracks.columns:
1706
- cell_status = f"phenotype: {self.df_tracks.loc[(self.df_tracks['FRAME']==self.current_frame)&(self.df_tracks['TRACK_ID'] == self.track_of_interest), self.status_name].to_numpy()[0]}\n"
1707
- else:
1708
- cell_status = f"phenotype: {self.df_tracks.loc[self.df_tracks['ID'] == self.track_of_interest, self.status_name].to_numpy()[0]}\n"
1709
- self.cell_info.setText(cell_selected + cell_status)
1710
- except Exception as e:
1711
- print(e)
1712
-
1713
- def create_new_event_class(self):
1714
-
1715
- # display qwidget to name the event
1716
- self.newClassWidget = CelldetectiveWidget()
1717
- self.newClassWidget.setWindowTitle("Create new characteristic group")
1718
-
1719
- layout = QVBoxLayout()
1720
- self.newClassWidget.setLayout(layout)
1721
- name_hbox = QHBoxLayout()
1722
- name_hbox.addWidget(QLabel("group name: "), 25)
1723
- self.class_name_le = QLineEdit("group")
1724
- name_hbox.addWidget(self.class_name_le, 75)
1725
- layout.addLayout(name_hbox)
1726
-
1727
- btn_hbox = QHBoxLayout()
1728
- submit_btn = QPushButton("submit")
1729
- cancel_btn = QPushButton("cancel")
1730
- btn_hbox.addWidget(cancel_btn, 50)
1731
- btn_hbox.addWidget(submit_btn, 50)
1732
- layout.addLayout(btn_hbox)
1733
-
1734
- submit_btn.clicked.connect(self.write_new_event_class)
1735
- cancel_btn.clicked.connect(self.close_without_new_class)
1736
-
1737
- self.newClassWidget.show()
1738
- center_window(self.newClassWidget)
1739
-
1740
- def apply_modification(self):
1741
- if self.time_of_interest_le.text() != "":
1742
- status = int(self.time_of_interest_le.text())
1743
- else:
1744
- status = 0
1745
- if "TRACK_ID" in self.df_tracks.columns:
1746
- self.df_tracks.loc[
1747
- (self.df_tracks["TRACK_ID"] == self.track_of_interest)
1748
- & (self.df_tracks["FRAME"] == self.current_frame),
1749
- self.status_name,
1750
- ] = status
1751
-
1752
- indices = self.df_tracks.index[
1753
- (self.df_tracks["TRACK_ID"] == self.track_of_interest)
1754
- & (self.df_tracks["FRAME"] == self.current_frame)
1755
- ]
1756
- else:
1757
- self.df_tracks.loc[
1758
- (self.df_tracks["ID"] == self.track_of_interest)
1759
- & (self.df_tracks["FRAME"] == self.current_frame),
1760
- self.status_name,
1761
- ] = status
1762
-
1763
- indices = self.df_tracks.index[
1764
- (self.df_tracks["ID"] == self.track_of_interest)
1765
- & (self.df_tracks["FRAME"] == self.current_frame)
1766
- ]
1767
-
1768
- self.df_tracks.loc[indices, self.status_name] = status
1769
- all_states = self.df_tracks.loc[:, self.status_name].tolist()
1770
- all_states = np.array(all_states)
1771
- self.state_color_map = color_from_state(all_states, recently_modified=False)
1772
-
1773
- self.df_tracks["group_color"] = self.df_tracks[self.status_name].apply(
1774
- self.assign_color_state
1775
- )
1776
- self.extract_scatter_from_trajectories()
1777
- self.give_cell_information()
1778
-
1779
- self.correct_btn.disconnect()
1780
- self.correct_btn.clicked.connect(self.show_annotation_buttons)
1781
-
1782
- self.hide_annotation_buttons()
1783
- self.correct_btn.setEnabled(False)
1784
- self.correct_btn.setText("correct")
1785
- self.cancel_btn.setEnabled(False)
1786
- self.del_shortcut.setEnabled(False)
1787
-
1788
- if len(self.selection) > 0:
1789
- self.selection.pop(0)
1790
-
1791
- self.draw_frame(self.current_frame)
1792
- self.fcanvas.canvas.draw()
1793
-
1794
- def assign_color_state(self, state):
1795
-
1796
- if np.isnan(state):
1797
- state = "nan"
1798
- return self.state_color_map[state]
1799
-
1800
- def draw_frame(self, framedata):
1801
- """
1802
- Update plot elements at each timestep of the loop.
1803
- """
1804
- self.framedata = framedata
1805
- self.frame_lbl.setText(f"position: {self.framedata}")
1806
- self.im.set_array(self.img)
1807
- self.status_scatter.set_offsets(self.positions[self.framedata])
1808
- # try:
1809
- self.status_scatter.set_edgecolors(self.colors[self.framedata][:, 0])
1810
- # except Exception as e:
1811
- # pass
1812
-
1813
- self.current_label = self.labels[self.current_frame]
1814
- self.current_label = contour_of_instance_segmentation(self.current_label, 5)
1815
-
1816
- self.im_mask.remove()
1817
- self.im_mask = self.ax.imshow(
1818
- np.ma.masked_where(self.current_label == 0, self.current_label),
1819
- cmap="viridis",
1820
- interpolation="none",
1821
- alpha=self.current_alpha,
1822
- vmin=0,
1823
- vmax=np.nanmax(self.labels.flatten()),
1824
- )
1825
-
1826
- return (
1827
- self.im,
1828
- self.status_scatter,
1829
- self.im_mask,
1830
- )
1831
-
1832
- def compute_status_and_colors(self):
1833
-
1834
- self.cancel_selection()
1835
-
1836
- if self.class_choice_cb.currentText() == "":
1837
- pass
1838
- else:
1839
- self.status_name = self.class_choice_cb.currentText()
1840
-
1841
- if self.status_name not in self.df_tracks.columns:
1842
- print("Creating a new status for visualization...")
1843
- self.make_status_column()
1844
- else:
1845
- print(f'Generating per-state colors for the status "{self.status_name}"...')
1846
- all_states = self.df_tracks.loc[:, self.status_name].tolist()
1847
- all_states = np.array(all_states)
1848
- self.state_color_map = color_from_state(all_states, recently_modified=False)
1849
- print(f'Color mapping for "{self.status_name}":')
1850
- pretty_table(self.state_color_map)
1851
- self.df_tracks["group_color"] = self.df_tracks[self.status_name].apply(
1852
- self.assign_color_state
1853
- )
1854
-
1855
- def make_status_column(self):
1856
- if self.status_name == "state_firstdetection":
1857
- pass
1858
- else:
1859
- self.df_tracks.loc[:, self.status_name] = 0
1860
- all_states = self.df_tracks.loc[:, self.status_name].tolist()
1861
- all_states = np.array(all_states)
1862
- self.state_color_map = color_from_state(all_states, recently_modified=False)
1863
- self.df_tracks["group_color"] = self.df_tracks[self.status_name].apply(
1864
- self.assign_color_state
1865
- )
1866
-
1867
- def extract_scatter_from_trajectories(self):
1868
-
1869
- self.positions = []
1870
- self.colors = []
1871
- self.tracks = []
1872
-
1873
- for t in np.arange(self.len_movie):
1874
- self.positions.append(
1875
- self.df_tracks.loc[
1876
- self.df_tracks["FRAME"] == t, ["POSITION_X", "POSITION_Y"]
1877
- ].to_numpy()
1878
- )
1879
- self.colors.append(
1880
- self.df_tracks.loc[
1881
- self.df_tracks["FRAME"] == t, ["group_color"]
1882
- ].to_numpy()
1883
- )
1884
- if "TRACK_ID" in self.df_tracks.columns:
1885
- self.tracks.append(
1886
- self.df_tracks.loc[
1887
- self.df_tracks["FRAME"] == t, "TRACK_ID"
1888
- ].to_numpy()
1889
- )
1890
- else:
1891
- self.tracks.append(
1892
- self.df_tracks.loc[self.df_tracks["FRAME"] == t, "ID"].to_numpy()
1893
- )
1894
-
1895
- def changed_class(self):
1896
- self.status_name = self.class_choice_cb.currentText()
1897
- if self.status_name != "":
1898
- self.compute_status_and_colors()
1899
- self.modify()
1900
- self.draw_frame(self.current_frame)
1901
- self.fcanvas.canvas.draw()
1902
-
1903
- def update_frame(self):
1904
- """
1905
- Update the displayed frame.
1906
- """
1907
- self.current_frame = self.frame_slider.value()
1908
- self.reload_frame()
1909
- if "TRACK_ID" in list(self.df_tracks.columns):
1910
- pass
1911
- elif "ID" in list(self.df_tracks.columns):
1912
- print("ID in cols... change class of interest... ")
1913
- self.track_of_interest = self.df_tracks[
1914
- self.df_tracks["FRAME"] == self.current_frame
1915
- ]["ID"].min()
1916
- self.modify()
1917
-
1918
- self.draw_frame(self.current_frame)
1919
- self.vmin = self.contrast_slider.value()[0]
1920
- self.vmax = self.contrast_slider.value()[1]
1921
- self.im.set_clim(vmin=self.vmin, vmax=self.vmax)
1922
- self.give_cell_information()
1923
-
1924
- self.fcanvas.canvas.draw()
1925
- self.plot_signals()
1926
-
1927
- def changed_channel(self):
1928
-
1929
- self.reload_frame()
1930
- self.contrast_slider.setRange(
1931
- *[np.nanpercentile(self.img, 0.001), np.nanpercentile(self.img, 99.999)]
1932
- )
1933
- self.contrast_slider.setValue(
1934
- [np.nanpercentile(self.img, 0.1), np.nanpercentile(self.img, 99.99)]
1935
- )
1936
- self.draw_frame(self.current_frame)
1937
- self.fcanvas.canvas.draw()
1938
-
1939
- def save_trajectories(self):
1940
- print(f"Saving trajectories !!")
1941
- if self.normalized_signals:
1942
- self.normalize_features_btn.click()
1943
- if self.selection:
1944
- self.cancel_selection()
1945
- self.df_tracks = self.df_tracks.drop(
1946
- self.df_tracks[self.df_tracks[self.status_name] == 99].index
1947
- )
1948
- # color_column = str(self.status_name) + "_color"
1949
- try:
1950
- self.df_tracks.drop(columns="", inplace=True)
1951
- except:
1952
- pass
1953
- try:
1954
- self.df_tracks.drop(columns="group_color", inplace=True)
1955
- except:
1956
- pass
1957
- try:
1958
- self.df_tracks.drop(columns="x_anim", inplace=True)
1959
- except:
1960
- pass
1961
- try:
1962
- self.df_tracks.drop(columns="y_anim", inplace=True)
1963
- except:
1964
- pass
1965
-
1966
- self.df_tracks.to_csv(self.trajectories_path, index=False)
1967
- print("Table successfully exported...")
1968
-
1969
- self.locate_tracks()
1970
- self.changed_class()
1971
-
1972
- def modify(self):
1973
- all_states = self.df_tracks.loc[:, self.status_name].tolist()
1974
- all_states = np.array(all_states)
1975
- self.state_color_map = color_from_state(all_states, recently_modified=False)
1976
-
1977
- self.df_tracks["group_color"] = self.df_tracks[self.status_name].apply(
1978
- self.assign_color_state
1979
- )
1980
-
1981
- self.extract_scatter_from_trajectories()
1982
- self.give_cell_information()
1983
-
1984
- self.correct_btn.disconnect()
1985
- self.correct_btn.clicked.connect(self.show_annotation_buttons)
1986
-
1987
- def reload_frame(self):
1988
- """
1989
- Load the frame from the current channel and time choice. Show imshow, update histogram.
1990
- """
1991
-
1992
- # self.clear_post_threshold_options()
1993
- self.previous_channel = self.current_channel
1994
- self.current_channel = self.choose_channel.currentIndex()
1995
-
1996
- t = int(self.frame_slider.value())
1997
- idx = t * self.nbr_channels + self.current_channel
1998
- self.img = load_frames(idx, self.stack_path, normalize_input=False)
1999
-
2000
- if self.previous_channel != self.current_channel:
2001
- # reinitialize intensity bounds
2002
- epsilon = 0.01
2003
- self.observed_min_intensity = 0
2004
- self.observed_max_intensity = 0 + epsilon
2005
-
2006
- if self.img is not None:
2007
- max_img = np.nanmax(self.img)
2008
- min_img = np.nanmin(self.img)
2009
- if max_img > self.observed_max_intensity:
2010
- self.observed_max_intensity = max_img
2011
- if min_img < self.observed_min_intensity:
2012
- self.observed_min_intensity = min_img
2013
- self.refresh_imshow()
2014
- # self.redo_histogram()
2015
- else:
2016
- print("Frame could not be loaded...")
2017
-
2018
- def refresh_imshow(self):
2019
- """
2020
-
2021
- Update the imshow based on the current frame selection.
2022
-
2023
- """
2024
-
2025
- if self.previous_channel != self.current_channel:
2026
-
2027
- self.vmin = np.nanpercentile(self.img.flatten(), 0.1)
2028
- self.vmax = np.nanpercentile(self.img.flatten(), 99.99)
2029
-
2030
- self.contrast_slider.disconnect()
2031
- self.contrast_slider.setRange(np.nanmin(self.img), np.nanmax(self.img))
2032
- self.contrast_slider.setValue([self.vmin, self.vmax])
2033
- self.contrast_slider.valueChanged.connect(self.contrast_slider_action)
2034
- else:
2035
- # self.contrast_slider.disconnect()
2036
- self.contrast_slider.setRange(
2037
- self.observed_min_intensity, self.observed_max_intensity
2038
- )
2039
- # self.contrast_slider.valueChanged.connect(self.contrast_slider_action)
2040
-
2041
- self.im.set_data(self.img)
2042
-
2043
- def del_cell(self):
2044
- self.time_of_interest_le.setEnabled(False)
2045
- self.time_of_interest_le.setText("99")
2046
- self.apply_modification()