batoolset-gui 0.2.0__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 (74) hide show
  1. batoolset/GUI/__init__.py +0 -0
  2. batoolset/GUI/customdialog.py +136 -0
  3. batoolset/GUI/exception_handler.py +74 -0
  4. batoolset/GUI/list_gui.py +514 -0
  5. batoolset/GUI/multi_image_display.py +1038 -0
  6. batoolset/GUI/open.py +325 -0
  7. batoolset/GUI/overlay_hints.py +45 -0
  8. batoolset/GUI/paint2.py +1369 -0
  9. batoolset/GUI/scrollablepaint.py +1165 -0
  10. batoolset/dialogs/__init__.py +0 -0
  11. batoolset/dialogs/opensave.py +228 -0
  12. batoolset/drawings/__init__.py +0 -0
  13. batoolset/drawings/math_text.py +294 -0
  14. batoolset/drawings/pdf_svg_renderer.py +376 -0
  15. batoolset/drawings/shapes/Position.py +183 -0
  16. batoolset/drawings/shapes/__init__.py +0 -0
  17. batoolset/drawings/shapes/automated_alignment_drawing_refactored.py +346 -0
  18. batoolset/drawings/shapes/circle2d.py +99 -0
  19. batoolset/drawings/shapes/compute_optimal_size_all_merged.py +328 -0
  20. batoolset/drawings/shapes/ellipse2d.py +336 -0
  21. batoolset/drawings/shapes/freehand2d.py +235 -0
  22. batoolset/drawings/shapes/group.py +1985 -0
  23. batoolset/drawings/shapes/image2d.py +3157 -0
  24. batoolset/drawings/shapes/line2d.py +579 -0
  25. batoolset/drawings/shapes/point2d.py +257 -0
  26. batoolset/drawings/shapes/polygon2d.py +357 -0
  27. batoolset/drawings/shapes/polyline2d.py +235 -0
  28. batoolset/drawings/shapes/rect2d.py +388 -0
  29. batoolset/drawings/shapes/rectangle2d.py +601 -0
  30. batoolset/drawings/shapes/scalebar.py +310 -0
  31. batoolset/drawings/shapes/serializablefont.py +133 -0
  32. batoolset/drawings/shapes/serializableguiparams.py +14 -0
  33. batoolset/drawings/shapes/square2d.py +102 -0
  34. batoolset/drawings/shapes/test_publication_style.py +220 -0
  35. batoolset/drawings/shapes/textorientation.py +45 -0
  36. batoolset/drawings/shapes/tools.py +467 -0
  37. batoolset/drawings/shapes/transform2D.py +350 -0
  38. batoolset/drawings/shapes/txt2d.py +615 -0
  39. batoolset/drawings/svg_clip_renderer.py +247 -0
  40. batoolset/drawings/svg_embed.py +165 -0
  41. batoolset/drawings/svg_text_renderer.py +183 -0
  42. batoolset/drawings/widgets/__init__.py +0 -0
  43. batoolset/drawings/widgets/col.py +343 -0
  44. batoolset/drawings/widgets/image_group.py +248 -0
  45. batoolset/drawings/widgets/image_organizer.py +96 -0
  46. batoolset/drawings/widgets/paint.py +184 -0
  47. batoolset/drawings/widgets/vectorial.py +256 -0
  48. batoolset/figure/__init__.py +0 -0
  49. batoolset/figure/alignment.py +822 -0
  50. batoolset/figure/column.py +1117 -0
  51. batoolset/figure/fig_tools.py +285 -0
  52. batoolset/figure/minimal_debugger_scale_etc.py +139 -0
  53. batoolset/figure/panel.py +697 -0
  54. batoolset/figure/row.py +852 -0
  55. batoolset/figure/user_selection.py +287 -0
  56. batoolset/icons/__init__.py +0 -0
  57. batoolset/icons/tools.py +248 -0
  58. batoolset/luts/__init__.py +0 -0
  59. batoolset/luts/lut_minimal_test.py +1308 -0
  60. batoolset/luts/lutcreatorfromformula.py +209 -0
  61. batoolset/luts/luteditor.py +243 -0
  62. batoolset/pyqts/__init__.py +0 -0
  63. batoolset/pyqts/pleasewaitdialog.py +14 -0
  64. batoolset/pyqts/tools.py +760 -0
  65. batoolset/serializations/__init__.py +0 -0
  66. batoolset/serializations/tools.py +1338 -0
  67. batoolset/theme.py +647 -0
  68. batoolset/uis/__init__.py +0 -0
  69. batoolset/uis/blinker.py +56 -0
  70. batoolset_gui-0.2.0.dist-info/METADATA +54 -0
  71. batoolset_gui-0.2.0.dist-info/RECORD +74 -0
  72. batoolset_gui-0.2.0.dist-info/WHEEL +5 -0
  73. batoolset_gui-0.2.0.dist-info/licenses/LICENSE +24 -0
  74. batoolset_gui-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1369 @@
1
+ # TODO maybe hide green pointer if mouse is out of the image --> dangerous
2
+ # NB this will ultimately become the one and only paint widget in the app --> all others or most of them should disappear and be replaced by that
3
+ # TODO need add the save but only for the mask edit item!!!
4
+ import os
5
+ from batoolset.settings.global_settings import set_UI # set the UI to qtpy
6
+ set_UI()
7
+ from batoolset.pyqts.tools import getCtrlModifier
8
+ import traceback
9
+ from qtpy.QtGui import QKeySequence
10
+ from skimage.measure import label, regionprops
11
+ from qtpy.QtCore import QRect, QTimer, Qt
12
+ from qtpy.QtWidgets import QWidget, QApplication, QMessageBox
13
+ from batoolset.dialogs.opensave import saveFileDialog
14
+ from batoolset.drawings.widgets.vectorial import VectorialDrawPane
15
+ from qtpy.QtWidgets import QMenu
16
+ from qtpy import QtCore, QtGui, QtWidgets
17
+ from qtpy.QtGui import QImage
18
+ from batoolset.img import toQimage, Img, get_white_bounds, RGB_to_int24, is_binary, auto_scale, save_as_tiff
19
+ from batoolset.ta.measurements.measurements3D.get_point_on_surface_if_centroid_is_bad import point_on_surface
20
+ from batoolset.ta.segmentation.neo_wshed import wshed
21
+ from batoolset.ta.selections.selection import get_colors_drawn_over, convert_selection_color_to_coords
22
+ from batoolset.files.tools import smart_name_parser
23
+ import numpy as np
24
+ from batoolset.tools.logger import TA_logger # logging
25
+
26
+ logger = TA_logger()
27
+
28
+ class Createpaintwidget(QWidget):
29
+ # auto_convert_float_to_binary_threshold = 0.4
30
+
31
+ default_mask_name = 'handCorrection.tif'
32
+
33
+ # maybe store
34
+ def __init__(self, enable_shortcuts=False): #, methods_overriding=None
35
+
36
+ super().__init__()
37
+ #
38
+ # if methods_overriding is not None:
39
+ # for k,v in methods_overriding.items():
40
+ # # replace internal method by external one
41
+ # k = v
42
+ # self.image_path = None
43
+ self.auto_contrast = False # boolean there to handle the always auto contrast???
44
+ self.save_path = None
45
+ self.vdp = VectorialDrawPane(active=False) # the vectorial drawing panel
46
+ self.raw_image = None # the original unprocessed image
47
+ self.image = None # the displayed image (a qimage representation of raw_image can be a subset of raw_image_also)
48
+ self.raw_mask = None # the original unprocessed mask
49
+ self.imageDraw = None # the mask image drawn over the image
50
+ self.raw_user_drawing = None # contains just the user drawing without image mask
51
+ self.cursor = None
52
+ self.maskVisible = True
53
+ self.scale = 1.0
54
+ self.drawing = False
55
+ self.brushSize = 3
56
+ self.minimal_cell_size = 10
57
+ self._clear_size = 30
58
+ self.drawColor = QtGui.QColor(QtCore.Qt.red) # blue green cyan
59
+ self.eraseColor = QtGui.QColor(QtCore.Qt.black)
60
+ self.eraseColor_visualizer = QtGui.QColor(QtCore.Qt.green)
61
+ self.cursorColor = QtGui.QColor(QtCore.Qt.green)
62
+ self.lastPoint = QtCore.QPointF()
63
+ self.change = False
64
+ self.propagate_mouse_move = False
65
+ # KEEP IMPORTANT required to track mouse even when not clicked
66
+ self.setMouseTracking(True) # KEEP IMPORTANT
67
+ self.scrollArea = None
68
+ self.statusBar = None
69
+ self.drawing_enabled = True
70
+ self.channel = None
71
+ self.force_cursor_visible = False # required to force activate cursor to be always visible
72
+ self.save_file_name=None
73
+ self.multichannel_mode = False
74
+
75
+ self.auto_convert_float_to_binary_threshold = 0.4
76
+
77
+ # self.multi_channel_editor_n_save = False
78
+ # TODO add sortcuts
79
+
80
+ if enable_shortcuts:
81
+ self.add_shortcuts() # TODO make this optional as it can cause trouble
82
+
83
+ def force_cursor_to_be_visible(self, boolean):
84
+ self.force_cursor_visible = boolean
85
+
86
+ # can be used for saving
87
+ def set_save_path(self, path):
88
+ self.save_path = path
89
+
90
+ def get_save_path(self):
91
+ return self.save_path
92
+
93
+ def set_scale(self, scale):
94
+ # print(self.scale) # 0.10000000000000014 --> this is still ok --> this value crashes 1.3877787807814457e-16
95
+
96
+ # make sure scale is bounded to avoid issues and non sense scalings
97
+ if scale<0.01:
98
+ scale = 0.01
99
+ if scale>50:
100
+ scale=50
101
+
102
+ self.scale = scale
103
+ self.vdp.scale =scale
104
+ # DO I NEED ADJUST MORE SCALES ???
105
+ # self.update()
106
+
107
+ def get_scale(self):
108
+ return self.scale
109
+
110
+ # comprend pas ça marche pas si parent
111
+ def add_shortcuts(self):
112
+ # zoomPlus = QtWidgets.QShortcut("Ctrl+Shift+=", self)
113
+ # zoomPlus.activated.connect(self.zoomIn)
114
+ # zoomPlus.setContext(QtCore.Qt.ApplicationShortcut) # make sure the shorctut always remain active
115
+ #
116
+ # zoomPlus2 = QtWidgets.QShortcut("Ctrl++", self)
117
+ # zoomPlus2.activated.connect(self.zoomIn)
118
+ # zoomPlus2.setContext(QtCore.Qt.ApplicationShortcut) # make sure the shorctut always remain active
119
+ #
120
+ # zoomMinus = QtWidgets.QShortcut("Ctrl+Shift+-", self)
121
+ # zoomMinus.activated.connect(self.zoomOut)
122
+ # zoomMinus.setContext(QtCore.Qt.ApplicationShortcut) # make sure the shorctut always remain active
123
+ #
124
+ # zoomMinus2 = QtWidgets.QShortcut("Ctrl+-", self)
125
+ # zoomMinus2.activated.connect(self.zoomOut)
126
+ # zoomMinus2.setContext(QtCore.Qt.ApplicationShortcut)
127
+
128
+ # enter_pressed = QtWidgets.QShortcut("Ctrl+Shift+=", self)
129
+ # zoomPlus.activated.connect(self.zoomIn)
130
+ # zoomPlus.setContext(QtCore.Qt.ApplicationShortcut) # make sure the shorctut always remain active
131
+
132
+
133
+ padEnterShortcut = QtWidgets.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Enter), self) # voila
134
+ padEnterShortcut.activated.connect(self.apply)
135
+ padEnterShortcut.setContext(QtCore.Qt.ApplicationShortcut) # make s
136
+
137
+ enterShortcut = QtWidgets.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Return), self) # voila
138
+ enterShortcut.activated.connect(self.apply)
139
+ enterShortcut.setContext(QtCore.Qt.ApplicationShortcut) # make s
140
+
141
+ enterShortcut2 = QtWidgets.QShortcut('Shift+Return', self) # voila
142
+ enterShortcut2.activated.connect(self.apply)
143
+ enterShortcut2.setContext(QtCore.Qt.ApplicationShortcut) # make s
144
+
145
+ # pb if parent has same shortcut then ignored
146
+ # the pb is really with enter --> maybe stolen focus by dialog ????
147
+ enterShortcut3 = QtWidgets.QShortcut('Shift+Enter', self)
148
+ enterShortcut3.activated.connect(self.apply)
149
+ enterShortcut3.setContext(QtCore.Qt.ApplicationShortcut) # make sure the shorctut always remain active
150
+
151
+
152
+ self.shrtM = QtWidgets.QShortcut("M", self)
153
+ self.shrtM.activated.connect(self.m_apply)
154
+ self.shrtM.setContext(QtCore.Qt.ApplicationShortcut)
155
+
156
+
157
+ self.ctrl_shift_S_grab_screen_shot = QtWidgets.QShortcut('Ctrl+Shift+S', self)
158
+ self.ctrl_shift_S_grab_screen_shot.activated.connect(self.grab_screen_shot)
159
+ self.ctrl_shift_S_grab_screen_shot.setContext(QtCore.Qt.ApplicationShortcut)
160
+
161
+ self.increase_contrastC = QtWidgets.QShortcut('C', self)
162
+ self.increase_contrastC.activated.connect(self.increase_contrast)
163
+ self.increase_contrastC.setContext(QtCore.Qt.ApplicationShortcut)
164
+
165
+ # QtGui.QShortcut(
166
+ # self.supr = QtWidgets.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Delete), self)
167
+
168
+ # QKeySequence(tr("Ctrl+X, Ctrl+C"))
169
+ # QKeySequence(Qt.CTRL + Qt.Key_X, Qt.CTRL + Qt.Key_C)
170
+
171
+ # self.supr = QtWidgets.QShortcut(QKeySequence(Qt.Key_Delete), self)
172
+ # self.supr.activated.connect(self.suppr_pressed)
173
+ # self.supr.setContext(QtCore.Qt.ApplicationShortcut)
174
+
175
+
176
+ # connect the m shortcut to show and hide mask
177
+
178
+ # this returns the coords of the cells drawn over --> very useful for clones, adding/removing cells such as dividing cells and dying cells !!!
179
+ # that seems to work very well --> TODO finalize that now or soon
180
+ def get_selection_coords_based_on_current_mask(self, bg_color=None):
181
+ coords_of_selection = []
182
+
183
+ # pb --> here I need do it on the labeled image -->
184
+
185
+ img_to_analyze = self.get_raw_image()
186
+ # si l'image est juste un mask binaire alors --> faut la convertir en label --> TODO
187
+ if is_binary(img_to_analyze):
188
+ if len(img_to_analyze.shape)==3:
189
+ # assume it's a handCorrection mask and the mask is 3channel
190
+ img_to_analyze = img_to_analyze[...,0]
191
+ img_to_analyze = label(img_to_analyze, connectivity=1, background=255) # will that always work
192
+ bg_color = 0
193
+
194
+ # why is this shit empty
195
+ mask=self.get_user_drawing()
196
+
197
+ # --> seems to be ok --> then why 0
198
+ # import matplotlib.pyplot as plt
199
+ # plt.imshow(img_to_analyze)
200
+ # plt.show()
201
+
202
+ selected_cells = get_colors_drawn_over(mask, img_to_analyze)
203
+ if not selected_cells:
204
+ logger.warning('No selection found --> nothing to do')
205
+ return coords_of_selection
206
+
207
+ self.set_mask(np.zeros_like(mask))
208
+ self.update()
209
+
210
+ tmp = img_to_analyze
211
+ if len(tmp.shape) == 3:
212
+ tmp = RGB_to_int24(tmp)
213
+ #
214
+ if bg_color is None:
215
+ if 0xFFFFFF in tmp:
216
+ bg_color = 0xFFFFFF
217
+ else:
218
+ bg_color = 0
219
+ #
220
+ # cell_label = label(tmp, connectivity=1, background=bg_color)
221
+ #
222
+ # # for all the selected cells --> return the coords --> probably a quite good idea...
223
+ # for region in regionprops(cell_label):
224
+ # color = tmp[region.coords[0][0], region.coords[0][1]]
225
+ # if color in selected_cells:
226
+ # point_awlays_inside_the_cell = point_on_surface(region, cell_label)
227
+ # coords_of_selection.append(point_awlays_inside_the_cell)
228
+ #
229
+ # return coords_of_selection
230
+
231
+ return convert_selection_color_to_coords(tmp, selected_cells=selected_cells, bg_color=bg_color)
232
+
233
+ # added this so that I can easily get user selection using a cell ID
234
+ def get_colors_drawn_over(self, forbidden_colors=[0,0xFFFFFF]):
235
+ mask = self.get_user_drawing()
236
+ # print('I was called')
237
+ if mask is None:
238
+ return
239
+
240
+ selected_colors = get_colors_drawn_over(mask, self.get_raw_image(),forbidden_colors=forbidden_colors)
241
+ # # print(mask)
242
+ # # need get all the colors of the raw image that correspond to user selection
243
+ # selected_colors = self.get_raw_image()[mask != 0]
244
+ # selected_colors = RGB_to_int24(selected_colors)
245
+ # selected_colors = set(selected_colors.ravel().tolist())
246
+ # # if 0xFFFFFF in selected_colors:
247
+ # # selected_colors.remove(0xFFFFFF)
248
+ # if forbidden_colors is not None:
249
+ # if not isinstance(forbidden_colors, list):
250
+ # forbidden_colors = [forbidden_colors]
251
+ # for color in forbidden_colors:
252
+ # if color in selected_colors:
253
+ # selected_colors.remove(color)
254
+ # selected_colors = list(selected_colors)
255
+ self.set_mask(np.zeros_like(mask))
256
+ self.update()
257
+ return selected_colors
258
+
259
+ # def suppr_pressed(self):
260
+
261
+ # or add another shortcut to force autocontrast all the time ???
262
+ def always_increase_contrast(self):
263
+ self.auto_contrast = not self.auto_contrast
264
+ print('Setting auto-contrast for all images to', self.auto_contrast)
265
+ if self.auto_contrast:
266
+ self.increase_contrast()
267
+
268
+ def increase_contrast(self):
269
+ # print('increasing contrast')
270
+ try:
271
+ # metadata = self.raw_image.metadata
272
+ # sqqsdq
273
+ # self.raw_image = Img(auto_threshold(self.raw_image),metadata=metadata)
274
+ # self.set_image(self.raw_image)
275
+ # shall I do it for the displayed image only ???? --> probably smarter porbably shoudl do a get display image that is independent of all other functions and handles all dims
276
+ # very dumb --> needs be done just for the dipslayed image and nothing else and --> maybe get the image back from the screen directly --> would be smarted and better
277
+
278
+ # maybe do this only on the displayed image --> using convert_qimage_to_numpy or do something that gets the image to be displayed including the channels
279
+ # or do a tool that will recover the image of interest with channels and also the rest of the dimensions --> MEGA TODO !!!
280
+ if not self.auto_contrast:
281
+ print('auto-increase contrast')
282
+ # hack to preserve luts --> TODO
283
+ copy = auto_scale(np.copy(self.raw_image))
284
+ meta = None
285
+ try:
286
+ meta=self.raw_image.metadata
287
+ except:
288
+ pass
289
+ # print(self.raw_image.metadata)
290
+ self.set_display(copy, metadata=meta)
291
+ except:
292
+ traceback.print_exc()
293
+
294
+ def grab_screen_shot(self):
295
+ # screenshot = QPixmap.grabWindow(self.winId())
296
+ output_file = saveFileDialog(parent_window=self, extensions="Supported Files (*.png);;All Files (*)",
297
+ default_ext='.png')
298
+ if output_file is not None:
299
+
300
+ try:
301
+ # we hide the cursor
302
+ self.cursor = QtGui.QImage(self.image.size(), QtGui.QImage.Format_ARGB32)
303
+ self.cursor.fill(QtCore.Qt.transparent)
304
+ self.update()
305
+ screenshot=self.grab()
306
+ screenshot.save(output_file, 'png')
307
+ # restore cursor
308
+ # or paint without cursor --> TODO do that better some day
309
+ except:
310
+ logger.error('Could not grab a screenshot...')
311
+
312
+ def save(self):
313
+ # save_img
314
+ print('ctrl S save method to override')
315
+
316
+ def apply(self):
317
+ print('apply method to override')
318
+
319
+ def shift_apply(self):
320
+ print('shift apply method to override')
321
+
322
+ def ctrl_m_apply(self):
323
+ print('ctrl m apply method to override')
324
+
325
+ def suppr_pressed(self):
326
+ # print('Suppr method to override')
327
+ if self.vdp.active:
328
+ self.vdp.removeCurShape()
329
+ self.update()
330
+ else:
331
+ print('Suppr method to override')
332
+
333
+
334
+ # def m_apply(self):
335
+ # print('m apply method to override')
336
+ # qsdsqqsdsq
337
+ def m_apply(self):
338
+ # print('m apply called')
339
+ self.maskVisible = not self.maskVisible
340
+ self.update()
341
+
342
+ def set_display(self, display, metadata=None):
343
+ # this actually sets the displayed image --> different from set imlage, can be used to show channels instead or stuff like that instead of the actula imge
344
+ if isinstance(display, np.ndarray):
345
+ display = toQimage(display, metadata=metadata)
346
+ self.image = display
347
+ self.update()
348
+
349
+ def set_image(self, img):
350
+ if img is None:
351
+ self.save_file_name = None
352
+ self.raw_image = None
353
+ self.image = None
354
+ self.imageDraw = None
355
+ self.raw_mask = None
356
+ self.raw_user_drawing = None
357
+ self.unsetCursor() # restore cursor
358
+ self.update()
359
+ return
360
+ else:
361
+ # remove cursor only if image is shown and not vectorial drawing mode!
362
+ if self.vdp.active:
363
+ self.unsetCursor()
364
+ else:
365
+ self.setCursor(Qt.BlankCursor)
366
+ # try:
367
+ # if isinstance(img, np.ndarray):
368
+ # self.image = img.getQimage() # bug is here
369
+ # # set an empty mask
370
+ # self.imageDraw = QtGui.QImage(self.image.size(), QtGui.QImage.Format_ARGB32)
371
+ # self.imageDraw.fill(QtCore.Qt.transparent)
372
+ # else:
373
+ # self.image = img[0].getQimage()
374
+ # # set the mask
375
+ # self.imageDraw = Img(self.createRGBA(img[1].getQimage()), dimensions='hwc').getQimage()
376
+ # except:
377
+ # NB THIS IS VERY REDUNDNAT ---> STICK TO ONE METHOD
378
+ if isinstance(img, str):
379
+ # self.image_path = img# maybe this image path can be used for saving, could also be provided serparately
380
+ self.save_file_name = smart_name_parser(img, ordered_output=self.default_mask_name)
381
+ img = Img(img)
382
+
383
+ # self.image_path=None # maybe this image path can be used for saving
384
+ if isinstance(img, np.ndarray):
385
+
386
+ # bug is exactly here --> why and where
387
+
388
+ # dimensions = None
389
+ # if isinstance(img, Img):
390
+ # dimensions = img.get_dimensions_as_string()
391
+
392
+ # self.image_path = None
393
+ self.raw_image = img
394
+
395
+ # print('tests',img.shape, isinstance(img,Img))
396
+
397
+ self.image = toQimage(img)# maybe this image path can be used for saving
398
+ if self.image is None:
399
+ logger.error('Image could not be displayed...')
400
+ self.unsetCursor()
401
+ self.update()
402
+ return
403
+ self.imageDraw = QtGui.QImage(self.image.size(), QtGui.QImage.Format_ARGB32)
404
+ self.imageDraw.fill(QtCore.Qt.transparent)
405
+ self.raw_user_drawing =QtGui.QImage(self.image.size(), QtGui.QImage.Format_ARGB32)
406
+ self.raw_user_drawing.fill(QtCore.Qt.transparent)
407
+ else:
408
+ # self.image_path = None
409
+ self.image = toQimage(img[0])
410
+ self.raw_image = img[0] #
411
+ # same as set mask --> how to do that
412
+ self.imageDraw = toQimage(Img(self.createRGBA(img[1]), dimensions='hwc'),preserve_alpha=True)#.getQimage()
413
+ self.raw_user_drawing = QtGui.QImage(self.imageDraw.size(), QtGui.QImage.Format_ARGB32)
414
+ self.raw_user_drawing.fill(QtCore.Qt.transparent)
415
+
416
+ if self.force_cursor_visible:
417
+ self.unsetCursor()
418
+
419
+ # self.image = QPixmap(100,200).toImage()
420
+ width = self.image.size().width()
421
+ height = self.image.size().height()
422
+ top = self.geometry().x()
423
+ left = self.geometry().y()
424
+ self.setGeometry(top, left, int(width*self.scale), int(height*self.scale))
425
+
426
+ self.cursor = QtGui.QImage(self.image.size(), QtGui.QImage.Format_ARGB32)
427
+ self.cursor.fill(QtCore.Qt.transparent)
428
+ self.update()
429
+
430
+ def get_raw_image(self):
431
+ return self.raw_image
432
+
433
+ def binarize(self, mask, auto_convert_float_to_binary=None, force=False):
434
+ if auto_convert_float_to_binary is None:
435
+ auto_convert_float_to_binary = self.auto_convert_float_to_binary_threshold
436
+ if auto_convert_float_to_binary and (mask.max() <= 1 or force):
437
+ # print('autoconvert')
438
+ mask = mask > auto_convert_float_to_binary
439
+ return mask
440
+
441
+ # do I need that ??? maybe yes if I just wanna change the mask
442
+ def set_mask(self, mask, auto_convert_float_to_binary=None):
443
+ if auto_convert_float_to_binary is None:
444
+ auto_convert_float_to_binary = self.auto_convert_float_to_binary_threshold
445
+ if isinstance(mask, str):
446
+ self.save_file_name = mask
447
+ # self.image_path = img# maybe this image path can be used for saving, could also be provided serparately
448
+ mask = Img(mask)
449
+ self.raw_mask = mask
450
+ # for TA compat
451
+ if mask.has_c():
452
+ # but should also save the raw mask as for the raw_image --> TODO --> maybe
453
+ if self.channel is not None:
454
+ mask=mask[...,self.channel]
455
+ else:
456
+ mask = mask[..., 0]
457
+
458
+ # convert mask to image draw
459
+ if mask is None:
460
+ self.imageDraw = None
461
+ self.raw_user_drawing = None
462
+ self.raw_mask = None
463
+ else:
464
+ # auto convert float deep learning masks to binary --> maybe this should be a parameter ...
465
+ # print('tada', auto_convert_float_to_binary, mask.max())
466
+
467
+ mask = self.binarize(mask, auto_convert_float_to_binary=auto_convert_float_to_binary)
468
+
469
+
470
+ # print('mask.shape',mask.shape)
471
+ # convert mask to a mask
472
+ # self.imageDraw = QtGui.QImage(self.image.size(), QtGui.QImage.Format_ARGB32)
473
+ # self.imageDraw.fill(QtCore.Qt.transparent)
474
+
475
+ # print('in here tmp ', mask.shape)
476
+
477
+
478
+ # if image has z channels and is more than just 2D / is 3D I could make this smarter by loading just the right image --> TODO --> sync the channel here with the channel of the currently dispalyer image
479
+ # SHALL I MAKE A FULL GUI FOR THAT ON RELY ON WHAT I HAVE ALREADY !!!!
480
+ self.imageDraw = toQimage(Img(self.createRGBA(mask), dimensions='hwc'),preserve_alpha=True) #.getQimage() # marche pas car besoin d'une ARGB
481
+ self.raw_user_drawing = QtGui.QImage(self.imageDraw.size(), QtGui.QImage.Format_ARGB32)
482
+ self.raw_user_drawing.fill(QtCore.Qt.transparent) # somehow this is really required to have an empty image otherwise it is not --> ??? why
483
+ self.update()
484
+
485
+
486
+ # do I have a way to just get what was drawn by the user and the bounds of it --> maybe by substratction --> or can I get the difference
487
+ # I need bounds of drawn stuff --> think about how to do that !!!
488
+ # we have used the user drawing for something --> let's reset it
489
+ # shall I also save the raw mask unperturbed by user drawing --> can be useful too in fact to do things with it
490
+ def reset_user_drawing(self):
491
+ if self.imageDraw is None:
492
+ self.raw_user_drawing = None
493
+ else:
494
+ self.raw_user_drawing = QtGui.QImage(self.imageDraw.size(), QtGui.QImage.Format_ARGB32)
495
+ self.raw_user_drawing.fill(QtCore.Qt.transparent)
496
+
497
+ # shall I draw any user interaction also in a separate file so that I can get just what was drawn without having the original mask ??? --> maybe a good idea --> call it get user drawing ???
498
+ def get_mask(self):
499
+ # nb will only work for red pen !!!! --> do not allow to change pen color
500
+ # pas mal mais image est RGBA
501
+ if self.imageDraw is not None:
502
+ # return Img.qimageToNumpy(self.imageDraw, mode='bnw8')
503
+ mask = self.convert_qimage_to_numpy(self.imageDraw)[..., 2]
504
+ # for some reason there are some 1 in the image --> needs a fix
505
+ mask[mask!=255]=0
506
+ return mask
507
+ else:
508
+ return None
509
+
510
+ def get_user_drawing(self, show_erased=False):
511
+ if self.raw_user_drawing is not None:
512
+
513
+
514
+
515
+ # bug here the user draing contains the original mask --> bug
516
+
517
+ all = self.convert_qimage_to_numpy(self.raw_user_drawing)
518
+
519
+ # plt.imshow(all)
520
+ # plt.show()
521
+
522
+ # no clue why this stuff is empty
523
+ # import matplotlib.pyplot as plt
524
+ # plt.imshow(all)
525
+ # plt.show()
526
+
527
+ drawn = all[..., 2]
528
+
529
+
530
+ # plt.imshow(erased) #--> ok
531
+ # plt.show()
532
+
533
+ # plt.imshow(drawn)
534
+ # plt.show()
535
+
536
+
537
+ drawn[drawn != 255] = 0
538
+
539
+ if show_erased:
540
+ erased = all[..., 1]
541
+ erased[erased != 255] = 0
542
+ return [erased, drawn]
543
+
544
+ return drawn
545
+ else:
546
+ return None
547
+
548
+ # from https://stackoverflow.com/questions/19902183/qimage-to-numpy-array-using-pyside
549
+ # nb there seems to be a bug in the order of the channels
550
+ def convert_qimage_to_numpy(self, qimage):
551
+ qimage = qimage.convertToFormat(QtGui.QImage.Format.Format_RGB32)
552
+ width = qimage.width()
553
+ height = qimage.height()
554
+ image_pointer = qimage.bits() # creates a deep copy --> this is what I want
555
+ try:
556
+ image_pointer.setsize(qimage.sizeInBytes()) # qt6 version of the stuff
557
+ except:
558
+ image_pointer.setsize(qimage.byteCount())
559
+
560
+
561
+ # arr = np.array(image_pointer,copy=True).reshape(height, width, 4)
562
+ arr = np.array(image_pointer).reshape(height, width, 4)
563
+ return arr
564
+
565
+ def createRGBA(self, handCorrection):
566
+
567
+
568
+
569
+
570
+
571
+ # print('Im called')
572
+ # use pen color to display the mask
573
+ # in fact I need to put the real color
574
+ # nb this will definitely not work if the user wants to have full 3D stuff
575
+
576
+ # shall I also offer the zooming on the objects --> ????
577
+
578
+
579
+
580
+
581
+ RGBA = np.zeros((handCorrection.shape[0], handCorrection.shape[1], 4), dtype=np.uint8)
582
+
583
+ red = self.drawColor.red()
584
+ green = self.drawColor.green()
585
+ blue = self.drawColor.blue()
586
+
587
+ #(handCorrection.shape[0], handCorrection.shape[1], 4))
588
+ # mask = handCorrection[handCorrection==255]
589
+
590
+ # ce truc est bleu --> pkoi
591
+ # RGBA[..., 0] = 255#handCorrection --> totalement transp --> transparence color --> GREEN --> BLUE
592
+ # RGBA[..., 1] = 0 # this is the red channel --> RED --> GREEN
593
+ # RGBA[..., 2] = 0 # rien ici --> tt transparent --> alpha ??? --> ALPHA -->RED
594
+ # RGBA[..., 3] = 255 # --> BLUE --> ALPHA --> ok
595
+
596
+ # pkoi je swappe les channels avant
597
+
598
+ # 255 partout --> blanc
599
+ # 0 255 255 0 --> rouge --> 1 = red channel and 2 = alpha --> weird
600
+
601
+ # RGBA[RGBA[..., 0] > 0, 0] = blue
602
+ # RGBA[RGBA[..., 1] > 0, 1] = green
603
+ # RGBA[RGBA[..., 2] > 0, 2] = red
604
+ # RGBA[..., 3] = 255 # ne s'affiche pas et je comprend pas pkoi
605
+
606
+ #BGR in fact --> need fix here or the other --> the one in
607
+
608
+
609
+ # how can I do that in a simpler way ???
610
+ # bug somewhere in qimage --> fix it some day --> due to bgra instead of RGBA
611
+ # assumes image has just the first
612
+ RGBA[handCorrection != 0, 0] = blue # b
613
+ # RGBA[..., 1] = handCorrection
614
+ RGBA[handCorrection != 0, 1] = green # g
615
+ RGBA[handCorrection != 0, 2] = red # r
616
+ RGBA[..., 3] = 255 # alpha --> indeed alpha
617
+ RGBA[handCorrection == 0, 3] = 0 # ça marche maintenant --> super complexe qd meme je trouve
618
+
619
+ # can I do it in one go --> I guess yes!!!
620
+ # RGBA[..., 3] = 255
621
+ # RGBA[handCorrection != 0] = blue,green,red, 0
622
+ # RGBA[handCorrection==0, 3] = 255
623
+
624
+ # ok mais plus transparent
625
+
626
+ #
627
+ # print(red, green, blue)
628
+
629
+ # RGBA[RGBA[..., 0] == 255] = red
630
+ # RGBA[RGBA[..., 1] == 255] = green
631
+ # RGBA[RGBA[..., 2] == 255] = blue
632
+
633
+ return RGBA
634
+
635
+ # TODO also implement channel change directly within the display tool
636
+ # if merge is applied --> apply on average of all channels --> maybe not so smart an idea but ok to start with and better than what I do in TA
637
+
638
+ # dunno if I put it here or not ???
639
+ # def get_nb_channels(self):
640
+ # if self.
641
+
642
+ # depreacted --> ideally should not be used
643
+ # TODO--> maybe implement a multi channel save -> TODO
644
+ # skip
645
+ def channelChange(self, i, skip_update_display=False):
646
+ # update displayed image depending on channel
647
+ # dqqsdqsdqsd
648
+ # pass
649
+ # try change channel if
650
+
651
+ # print('in channel change !!!')
652
+ # if image_to_display is None:
653
+ # image_to_display = self.raw_image
654
+ if self.raw_image is not None:
655
+ # print('in', self.img.metadata)
656
+ # if self.Stack.currentIndex() == 0:
657
+ # need copy the image --> implement that
658
+ # print(self.img[..., i].copy())
659
+ # print(self.img[..., i])
660
+ if i == 0 and not skip_update_display:
661
+ self.set_display(self.raw_image)
662
+ self.channel = None
663
+ # print('original', self.img.metadata)
664
+ else:
665
+ if not skip_update_display:
666
+ # print('modified0', self.img.metadata)
667
+ # I need a hack when the image is single channel yet I need several masks for it !!!
668
+ meta = None
669
+ try:
670
+ meta = self.raw_image
671
+ except:
672
+ pass
673
+ if self.multichannel_mode and i-1>=self.raw_image.shape[-1]:
674
+ channel_img = self.raw_image.imCopy(c=0) # if out of bonds load the first channel
675
+ else:
676
+ channel_img = self.raw_image.imCopy(c=i - 1) # it's here that it is affected
677
+ self.set_display(
678
+ channel_img, metadata=meta) # maybe do a set display instead rather --> easier to handle --> does a subest of the other
679
+ self.channel = i-1
680
+ # print('modified1', self.img.metadata)
681
+ # print('modified2', channel_img.metadata)
682
+
683
+ if self.multichannel_mode:
684
+ if self.raw_mask is not None:
685
+ # print('multichannel_mode', self.multichannel_mode)
686
+ try:
687
+ # print('self.raw_mask.shape',self.raw_mask.shape)
688
+ if len(self.raw_mask.shape)<=2:
689
+ self.raw_mask = self.raw_mask[..., np.newaxis]
690
+ self.set_mask(self.raw_mask[..., i-1])
691
+ except:
692
+ # print(i-1, self.raw_mask.shape[-1] , len(self.raw_mask.shape))
693
+ if i-1 >= self.raw_mask.shape[-1] and len(self.raw_mask.shape)>2:
694
+ # extend mask
695
+
696
+
697
+ if len(self.raw_mask.shape) > 2:
698
+ raw_mask2 = np.zeros_like(self.raw_mask, shape=(*self.raw_mask.shape[0:-1], i))
699
+ else:
700
+ raw_mask2 = np.zeros_like(self.raw_mask, shape=(*self.raw_mask.shape, i))
701
+
702
+ # copy existing mask to it
703
+ # print(raw_mask2.shape, self.raw_mask.shape)
704
+ for ch in range(self.raw_mask.shape[-1]):
705
+ raw_mask2[...,ch]=self.raw_mask[...,ch]
706
+ self.raw_mask = raw_mask2
707
+ self.set_mask(self.raw_mask[..., i - 1])
708
+ else:
709
+ traceback.print_exc()
710
+ else:
711
+ # create empty mask
712
+ if len(self.raw_image.shape)>2:
713
+ self.raw_mask = np.zeros(shape=(*self.raw_image.shape[0:-1],i))
714
+ else:
715
+ self.raw_mask = np.zeros(shape=(*self.raw_image.shape, i))
716
+
717
+ # print('doubs', self.raw_mask.shape, self.raw_image.shape)
718
+ self.set_mask(self.raw_mask[..., i - 1])
719
+ self.update()
720
+ else:
721
+ self.channel = None
722
+ # else:
723
+ # # logger.error("Not implemented yet TODO add support for channels in 3D viewer")
724
+ # # sdqdqsdsqdqsd
725
+ # self.loadVolume()
726
+
727
+
728
+ def get_colors_drawn_over(self):
729
+ mask = self.get_user_drawing()
730
+ # print('I was called')
731
+ if mask is None:
732
+ return []
733
+ # print(mask)
734
+
735
+ # need get all the colors of the raw image that correspond to user selection
736
+ selected_colors =self.get_raw_image()[mask!=0]
737
+ selected_colors = RGB_to_int24(selected_colors)
738
+
739
+ selected_colors =set(selected_colors.ravel().tolist())
740
+ if 0xFFFFFF in selected_colors:
741
+ selected_colors.remove(0xFFFFFF)
742
+ selected_colors = list(selected_colors)
743
+ # this is the only way I found to reset the user drawing but this is dirty and I must find a better way
744
+ self.set_mask(np.zeros_like(mask))
745
+
746
+ self.update()
747
+ return selected_colors
748
+
749
+ def edit_drawing(self):
750
+ # if self.multichannel_mode and self.channel is None:
751
+ # logger.error('please select a channel first')
752
+ # return
753
+
754
+ # basic mask drawing edit
755
+ drawn_mask = self.get_mask()
756
+ if drawn_mask is None:
757
+ return
758
+ drawn_mask = np.copy(drawn_mask)
759
+ erased, drawn = self.get_user_drawing(show_erased=True)
760
+ # bounds = get_white_bounds([drawn, erased])
761
+ # if bounds is None:
762
+ # pass
763
+ # print(drawn_mask.shape)
764
+ # print(drawn_mask.max(), drawn_mask.min())
765
+ # print(drawn_mask.dtype)
766
+
767
+ drawn_mask[erased!=0]=0
768
+ drawn_mask[drawn!=0]=drawn[drawn!=0]
769
+ if self.multichannel_mode:
770
+ if self.raw_mask is not None:
771
+ if self.channel is not None:
772
+ # print('overwriting mask in image')
773
+ self.raw_mask[..., self.channel]=drawn_mask
774
+ # big bug here this stuff contains the bg image --> really huge bug
775
+ # Img(self.raw_mask, dimensions='hwc').save('/E/Sample_images/sample_images_PA/test_complete_wing_raphael/tst2.tif')
776
+ else:
777
+ # if nothing is specified assume ch 0
778
+ if len(self.raw_mask.shape)>2:
779
+ self.raw_mask[..., 0] = drawn_mask
780
+ else:
781
+ self.raw_mask = drawn_mask
782
+
783
+ # Img(drawn_mask, dimensions='hw').save('/E/Sample_images/sample_images_PA/test_complete_wing_raphael/tst.tif')
784
+ self.set_mask(Img(drawn_mask, dimensions='hw'))
785
+
786
+
787
+ # draws/removes bonds locally à la TA
788
+ def apply_drawing(self, minimal_cell_size=0):
789
+ # print('minimal_cell_size', minimal_cell_size)
790
+
791
+ drawn_mask = self.get_mask()
792
+ # print(drawn_mask)
793
+ # no image or nothing drawn --> return
794
+ if drawn_mask is None:
795
+ return
796
+ erased, drawn = self.get_user_drawing(show_erased=True)
797
+ bounds = get_white_bounds([drawn, erased])
798
+ if bounds is None:
799
+ # in case the guy presses shift apply with no mask then run it over the whole image --> can make sense to have this
800
+ if minimal_cell_size is not None and minimal_cell_size !=0:
801
+ # dirty way of changing size --> needs be improved
802
+ minimal_cell_size = self.minimal_cell_size
803
+
804
+
805
+ # print('minimal_cell_size2', minimal_cell_size)
806
+ drawn_mask = wshed(drawn_mask, seeds='mask', min_seed_area=minimal_cell_size)
807
+ self.set_mask(drawn_mask)
808
+ return
809
+
810
+ if np.count_nonzero(erased) != 0:
811
+ lab_erased = label(erased, connectivity=2, background=0)
812
+ rps_lab_erased = regionprops(lab_erased)
813
+
814
+ lab_drawn_mask = label(drawn_mask, connectivity=1, background=255)
815
+ rps_lab_drawn_mask = regionprops(lab_drawn_mask)
816
+
817
+ for erased_bond in rps_lab_erased:
818
+ ids = np.unique(lab_drawn_mask[erased_bond.coords[:, 0], erased_bond.coords[:, 1]])
819
+ for id in ids:
820
+ if id != 0:
821
+ cur_bounds = rps_lab_drawn_mask[id - 1].bbox
822
+ bounds[0] = min(bounds[0], cur_bounds[0])
823
+ bounds[1] = max(bounds[1], cur_bounds[2])
824
+ bounds[2] = min(bounds[2], cur_bounds[1])
825
+ bounds[3] = max(bounds[3], cur_bounds[3])
826
+
827
+ min_y, max_y, min_x, max_x = bounds
828
+
829
+ first_x = 128
830
+ first_y = 128
831
+ min_x = min_x - 128
832
+ max_x = max_x + 128
833
+ min_y = min_y - 128
834
+ max_y = max_y + 128
835
+
836
+ # TODO fix upper bond too
837
+
838
+ if min_x < 0:
839
+ first_x = 128 + min_x
840
+ min_x = 0
841
+ if min_y < 0:
842
+ first_y = 128 + min_y
843
+ min_y = 0
844
+ if max_y > drawn_mask.shape[0]:
845
+ max_y = drawn_mask.shape[0]
846
+ if max_x > drawn_mask.shape[1]:
847
+ max_x = drawn_mask.shape[1]
848
+
849
+ minished = wshed(drawn_mask[min_y:max_y, min_x:max_x], seeds='mask', min_seed_area=minimal_cell_size)
850
+ drawn_mask[min_y:max_y, min_x:max_x][first_y:min(first_y + (bounds[1] - bounds[0]) + 1, minished.shape[0]),first_x:min(first_x + (bounds[3] - bounds[2]) + 1, minished.shape[1])] = minished[first_y:min(first_y + (bounds[1] - bounds[0]) + 1, minished.shape[0]), first_x:min(first_x + (bounds[3] - bounds[2]) + 1, minished.shape[1])]
851
+ self.set_mask(drawn_mask)
852
+
853
+ def force_consistent_range(self, img, auto_convert_float_to_binary=None):
854
+ if auto_convert_float_to_binary is None:
855
+ auto_convert_float_to_binary = self.auto_convert_float_to_binary_threshold
856
+ # for ch in range(img.shape[-1]):
857
+ # tmp = img[...,ch]
858
+ # min = tmp.min()
859
+ # max = tmp.max()
860
+ # # if min!=max:
861
+ # # change range of the stuff
862
+ # # print('error in range')
863
+ # if max<=1.0:
864
+ # print('error in range')
865
+ # print(min, max)
866
+ # print(min, max)
867
+ # img =
868
+ return self.binarize(img, auto_convert_float_to_binary=auto_convert_float_to_binary, force=True)
869
+
870
+ def save_mask(self, multichannel_save=False, forced_nb_of_channels=None):
871
+ # print('saving to ....' + self.save_file_name)
872
+ if self.save_file_name is not None:
873
+ # print('really saving/...')
874
+ mask = self.get_mask()
875
+ if mask is not None:
876
+ logger.debug('saving mask to '+str(self.save_file_name))
877
+ try:
878
+ single_channel_image = len(self.raw_image.shape)==2
879
+ except:
880
+ single_channel_image = False
881
+ if multichannel_save == False or single_channel_image and (forced_nb_of_channels is None or forced_nb_of_channels ==0):
882
+ Img(mask, dimensions='hw').save(self.save_file_name, compression='zlib')
883
+ else:
884
+ if self.raw_mask is not None:
885
+ # print('quick saving edited mask')
886
+ # Img(self.raw_mask, dimensions='hwc').save(self.save_file_name)
887
+
888
+ if len(self.raw_mask.shape)>2:
889
+ # I need check all channels have the same normalization
890
+ Img(self.force_consistent_range(self.raw_mask).astype(np.uint8)*255, dimensions='hwc').save(self.save_file_name, compression='zlib')
891
+ else:
892
+ Img(self.raw_mask, dimensions='hw').save(self.save_file_name, compression='zlib')
893
+ return
894
+
895
+ # print('test', self.channel )
896
+ # print('orig img channels', self.raw_image.shape[-1])
897
+ # if self.channel is None:
898
+ # logger.error('Please select a channel first')
899
+ # return
900
+ # BELOW IS CRAPPY CODE --> BEST is if multichannel_mode to create a black empty image
901
+ # if os.path.exists(self.save_file_name):
902
+ # try:
903
+ # mask_out = Img(self.save_file_name)
904
+ # if self.channel is not None:
905
+ # mask_out[...,self.channel]=mask
906
+ # else:
907
+ # # assume ch0
908
+ # mask_out[..., 0] = mask
909
+ # Img(mask_out, dimensions='hwc').save(self.save_file_name)
910
+ # return
911
+ # except:
912
+ # traceback.print_exc()
913
+ # print('le fichier existe')
914
+ #
915
+ # # on le charge
916
+ # # else:
917
+ # print('le fichier existe pas')
918
+ # mask_out = np.zeros_like(mask, shape=(*mask.shape, self.raw_image.shape[-1]))
919
+ # if self.channel is not None:
920
+ # mask_out[..., self.channel] = mask
921
+ # else:
922
+ # # assume ch0
923
+ # mask_out[..., 0] = mask
924
+ # Img(mask_out, dimensions='hwc').save(self.save_file_name)
925
+ # sinon on recup le nb de canaux de l'image finale
926
+
927
+
928
+ # def _update_channel(self, output, mask, channel):
929
+ # if channel is not None:
930
+ # mask_out = np.zeros_like(mask, shape=(*mask.shape, self.raw_image.shape[-1]))
931
+ # mask_out[..., self.channel] = mask
932
+ # Img(mask_out, dimensions='hwc').save(self.save_file_name)
933
+
934
+
935
+
936
+
937
+
938
+ # this is the local seeded watershed à la TA
939
+ def manually_reseeded_wshed(self):
940
+
941
+ if self.get_raw_image() is None:
942
+ return
943
+
944
+ try:
945
+ if self.channel is None and self.get_raw_image().has_c():
946
+ logger.error('Please select a channel fisrt')
947
+ return
948
+ except:
949
+ print(self.get_raw_image().shape, self.channel)
950
+ if self.channel is None and len(self.get_raw_image().shape)>=3 and self.get_raw_image().shape[-1]>1:
951
+ logger.error('Please select a channel fisrt')
952
+ return
953
+
954
+ usr_drawing = self.get_user_drawing()
955
+ labs = label(usr_drawing, connectivity=2, background=0)
956
+
957
+ # pb here because the user drawing also contains the mask and I don't want it
958
+ # plt.imshow(labs)
959
+ # plt.show()
960
+
961
+ rps_user_drawing = regionprops(labs)
962
+ if len(rps_user_drawing) == 0:
963
+ return
964
+
965
+ drawn_mask = self.get_mask()
966
+
967
+ # plt.imshow(drawn_mask)
968
+ # plt.show()
969
+
970
+ drawn_mask[labs != 0] = 0
971
+ lab_cells_user_drawing = label(drawn_mask, connectivity=1, background=255)
972
+ rps_cells_user_drawing = regionprops(lab_cells_user_drawing)
973
+
974
+ min_y = 100000000
975
+ min_x = 100000000
976
+ max_y = 0
977
+ max_x = 0
978
+ for rps_user in rps_user_drawing:
979
+ label_id = lab_cells_user_drawing[rps_user.coords[0][0], rps_user.coords[0][1]]
980
+ bbox = rps_cells_user_drawing[label_id - 1].bbox
981
+ min_y = min(bbox[0], min_y)
982
+ min_x = min(bbox[1], min_x)
983
+ max_y = max(bbox[2], max_y)
984
+ max_x = max(bbox[3], max_x)
985
+
986
+
987
+
988
+
989
+ # here I need get the channel in fact
990
+ img = self.get_raw_image()[min_y:max_y + 1, min_x:max_x + 1]
991
+ if self.channel is not None:
992
+ img = img[..., self.channel]
993
+ minished = wshed(img,
994
+ seeds=labs[min_y:max_y + 1, min_x:max_x + 1], weak_blur=1.)
995
+ drawn_mask[min_y:max_y + 1, min_x:max_x + 1][minished != 0] = minished[minished != 0]
996
+ # plt.imshow(drawn_mask)
997
+ # plt.show()
998
+ self.set_mask(drawn_mask)
999
+
1000
+
1001
+ def mousePressEvent(self, event):
1002
+ if not self.hasMouseTracking() or not self.drawing_enabled:
1003
+ return
1004
+ self.clickCount = 1
1005
+ if self.vdp.active:
1006
+ self.vdp.mousePressEvent(event)
1007
+ self.update()
1008
+ return
1009
+
1010
+ if event.buttons() == QtCore.Qt.LeftButton or event.buttons() == QtCore.Qt.RightButton:
1011
+ self.drawing = True
1012
+ zoom_corrected_pos = event.position() / self.scale
1013
+ self.lastPoint = zoom_corrected_pos
1014
+ self.drawOnImage(event)
1015
+ else:
1016
+ self.drawing = False
1017
+
1018
+
1019
+ # can I pass this event to something else to synchronize the mouse moves
1020
+ def mouseMoveEvent(self, event):
1021
+ if not self.hasMouseTracking() or not self.drawing_enabled:
1022
+ return
1023
+ else:
1024
+ if self.propagate_mouse_move == True:
1025
+ self.propagate_mouse_event(event)
1026
+
1027
+ # print('in mouse move', self.hasMouseTracking(), self.drawing, self.vdp.active)
1028
+ if self.statusBar:
1029
+ zoom_corrected_pos = event.position() / self.scale
1030
+ self.statusBar.showMessage('x=' + str(zoom_corrected_pos.x()) + ' y=' + str(
1031
+ zoom_corrected_pos.y()))
1032
+ if self.vdp.active:
1033
+ self.vdp.mouseMoveEvent(event)
1034
+ region = self.scrollArea.widget().visibleRegion()
1035
+ self.update(region)
1036
+ return
1037
+ self.drawOnImage(event)
1038
+
1039
+ def mouseReleaseEvent(self, event):
1040
+ self.drawing = False
1041
+ if not self.hasMouseTracking() or not self.drawing_enabled:
1042
+ return
1043
+ if self.vdp.active:
1044
+ self.vdp.mouseReleaseEvent(event)
1045
+ self.update() # required to update drawing
1046
+ return
1047
+ # if event.button == QtCore.Qt.LeftButton:
1048
+ # self.drawing = False
1049
+ if self.clickCount == 1:
1050
+ QTimer.singleShot(QApplication.instance().doubleClickInterval(),
1051
+ self.updateButtonCount)
1052
+
1053
+ def propagate_mouse_event(self, event):
1054
+ print(self,event)
1055
+ pass
1056
+
1057
+ def drawOnImage(self, event):
1058
+ # bug fix for image drawing when image is none
1059
+ if self.imageDraw is None:
1060
+ return
1061
+ zoom_corrected_pos = event.position() / self.scale
1062
+ if self.drawing and (event.buttons() == QtCore.Qt.LeftButton or event.buttons() == QtCore.Qt.RightButton):
1063
+ # now drawing or erasing over the image
1064
+ self._draw_on_image(self.imageDraw, event, zoom_corrected_pos)
1065
+ # this contains only the user drawing and nothing else --> can be useful to have too!!!
1066
+ self._draw_on_image(self.raw_user_drawing, event, zoom_corrected_pos, erase_color=self.eraseColor_visualizer)
1067
+
1068
+
1069
+
1070
+ # Drawing the cursor TODO add boolean to ask if drawing cursor should be shown
1071
+ painter = QtGui.QPainter(self.cursor)
1072
+ try:
1073
+ # We erase previous pointer
1074
+ r = QtCore.QRect(QtCore.QPoint(), self._clear_size * QtCore.QSize() * self.brushSize) # WHY DO I START FROM 0, 0 (empty point I should have taken old coord of self.lastPoint!!! MEGA TODO
1075
+ painter.save()
1076
+ r.moveCenter(self.lastPoint.toPoint())
1077
+ painter.setCompositionMode(QtGui.QPainter.CompositionMode_Clear)
1078
+ painter.eraseRect(r)
1079
+ painter.restore()
1080
+ # draw the new one
1081
+
1082
+ stroke_size = 2
1083
+ if self.brushSize<6:
1084
+ stroke_size = 1
1085
+
1086
+ # if is 1 --> I should really draw a point instead
1087
+ # I should also hide the cursor --> that would help
1088
+ painter.setPen(QtGui.QPen(self.cursorColor, stroke_size, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap,
1089
+ QtCore.Qt.RoundJoin))
1090
+ if self.brushSize >1:
1091
+ painter.drawEllipse(zoom_corrected_pos, int(self.brushSize / 2.),
1092
+ int(self.brushSize / 2.))
1093
+ else:
1094
+ painter.drawPoint(zoom_corrected_pos)
1095
+ except:
1096
+ traceback.print_exc()
1097
+ finally:
1098
+ painter.end()
1099
+ try:
1100
+ region = self.scrollArea.widget().visibleRegion()
1101
+ except:
1102
+ # assume no scroll area --> use self visible region
1103
+ region =self.visibleRegion()
1104
+
1105
+ self.update(region)
1106
+
1107
+ # required to erase mouse pointer
1108
+ self.lastPoint = zoom_corrected_pos
1109
+
1110
+
1111
+ def _draw_on_image(self, image_to_be_drawn, event, zoom_corrected_pos, erase_color=None):
1112
+ painter = QtGui.QPainter(image_to_be_drawn)
1113
+ try:
1114
+ # small hack to allow use pen or fingers on touch screens to erase when Ctrl or shift is pressed along with the click --> MAYBE PUT THIS AS AN OPTION SOME DAY BECAUSE THIS MAY PERTURB PEOPLE
1115
+ if event.buttons() == QtCore.Qt.LeftButton and not (event.modifiers() == getCtrlModifier() or event.modifiers() ==QtCore.Qt.ShiftModifier):
1116
+ painter.setPen(QtGui.QPen(self.drawColor, self.brushSize, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap,
1117
+ QtCore.Qt.RoundJoin))
1118
+ else:
1119
+ if erase_color is None:
1120
+ erase_color = self.eraseColor
1121
+ painter.setPen(QtGui.QPen(erase_color, self.brushSize, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap,
1122
+ QtCore.Qt.RoundJoin))
1123
+ if self.lastPoint != zoom_corrected_pos:
1124
+ painter.drawLine(self.lastPoint, zoom_corrected_pos)
1125
+ else:
1126
+ # if zero length line then draw point instead
1127
+ painter.drawPoint(zoom_corrected_pos)
1128
+ except:
1129
+ traceback.print_exc()
1130
+ finally:
1131
+ painter.end()
1132
+
1133
+ # adds context/right click menu but only in vectorial mode
1134
+ def contextMenuEvent(self, event):
1135
+ if not self.vdp.active:
1136
+ return
1137
+ cmenu = QMenu(self)
1138
+ newAct = cmenu.addAction("New")
1139
+ opnAct = cmenu.addAction("Open")
1140
+ quitAct = cmenu.addAction("Quit")
1141
+ action = cmenu.exec_(self.mapToGlobal(event.pos()))
1142
+ if action == quitAct:
1143
+ sys.exit(0)
1144
+
1145
+ def updateButtonCount(self):
1146
+ self.clickCount = 1
1147
+
1148
+ def mouseDoubleClickEvent(self, event):
1149
+ self.clickCount = 2
1150
+
1151
+ # TODO maybe add controls to be sure we are in drawing mode with a pen ???
1152
+ # double click fill or delete
1153
+ if not self.vdp.active:
1154
+ zoom_corrected_pos = event.position() / self.scale
1155
+ if event.buttons() == QtCore.Qt.LeftButton:
1156
+ # double click fill
1157
+ if self.dialog_fill_erase('fill'):
1158
+ self.double_click_fill(zoom_corrected_pos)
1159
+ elif event.buttons() == QtCore.Qt.RightButton:
1160
+ # double click delete shape
1161
+ if self.dialog_fill_erase('erase'):
1162
+ self.double_click_erase(zoom_corrected_pos)
1163
+
1164
+ self.vdp.mouseDoubleClickEvent(event)
1165
+
1166
+ def dialog_fill_erase(self, action='fill'):
1167
+ choice = QMessageBox.question(self, 'Double click detected!',
1168
+ 'Do you want to '+ action +' the shape?',
1169
+ QMessageBox.Yes | QMessageBox.No)
1170
+ if choice == QMessageBox.Yes:
1171
+ return True
1172
+ else:
1173
+ return False
1174
+
1175
+
1176
+ def double_click_fill(self, zoom_corrected_pos):
1177
+ try:
1178
+ # erased, drawn = self.get_user_drawing(show_erased=True)
1179
+ usr_drawing = self.get_user_drawing()
1180
+ labs = label(usr_drawing, connectivity=2, background=0)
1181
+ # do I really need that can I rather do np.unique or alike and will it be faster
1182
+ rps_user_drawing = regionprops(labs)
1183
+ if len(rps_user_drawing) == 0:
1184
+ return
1185
+ drawn_mask = self.get_mask()
1186
+ # cell clicked over
1187
+ cell_id_clicked_over = labs[int(zoom_corrected_pos.y()), int(zoom_corrected_pos.x())]
1188
+ if cell_id_clicked_over == 0:
1189
+ return
1190
+ # we remove the cell drawn over
1191
+ drawn_mask[labs==cell_id_clicked_over] = 0
1192
+ # maintenant j'inverse et je trouve la cellule à filler sur le negatif
1193
+ lab_cells_user_drawing = label(drawn_mask, connectivity=1, background=255)
1194
+ cell_id_clicked_over2 = lab_cells_user_drawing[int(zoom_corrected_pos.y()), int(zoom_corrected_pos.x())]
1195
+ drawn_mask[lab_cells_user_drawing==cell_id_clicked_over2]=255
1196
+ self.set_mask(drawn_mask)
1197
+ except:
1198
+ logger.error('Double click fill failed')
1199
+ traceback.print_exc()
1200
+
1201
+
1202
+ def double_click_erase(self, zoom_corrected_pos):
1203
+ try:
1204
+ # erased, drawn = self.get_user_drawing(show_erased=True)
1205
+ erased, drawn = self.get_user_drawing(show_erased=True)
1206
+ labs = label(erased, connectivity=2, background=0)
1207
+
1208
+ # do I really need that can I rather do np.unique or alike and will it be faster
1209
+ rps_user_drawing = regionprops(labs)
1210
+ if len(rps_user_drawing) == 0:
1211
+ return
1212
+ drawn_mask = self.get_mask()
1213
+ # cell clicked over
1214
+ cell_id_clicked_over = labs[int(zoom_corrected_pos.y()), int(zoom_corrected_pos.x())]
1215
+ if cell_id_clicked_over==0:
1216
+ return
1217
+ # we refill the removed cell
1218
+ drawn_mask[labs==cell_id_clicked_over] = 255
1219
+ # maintenant j'inverse et je trouve la cellule à filler sur le negatif
1220
+ lab_cells_user_drawing = label(drawn_mask, connectivity=1, background=0)
1221
+ cell_id_clicked_over2 = lab_cells_user_drawing[int(zoom_corrected_pos.y()), int(zoom_corrected_pos.x())]
1222
+ drawn_mask[lab_cells_user_drawing==cell_id_clicked_over2]=0
1223
+
1224
+
1225
+ self.set_mask(drawn_mask)
1226
+ except:
1227
+ logger.error('Double click delete failed')
1228
+ traceback.print_exc()
1229
+
1230
+
1231
+ def paintEvent(self, event):
1232
+ # super(Createpaintwidget, self).paintEvent(event) # KEEP somehow tjis causes bugs --> do not uncomment it
1233
+ canvasPainter = QtGui.QPainter(self)
1234
+ try:
1235
+
1236
+ # the scrollpane visible region
1237
+ try:
1238
+ visibleRegion = self.scrollArea.widget().visibleRegion()
1239
+ except:
1240
+ #assume no scroll region --> visible region is self visible region
1241
+ visibleRegion = self.visibleRegion()
1242
+ # the corresponding rect
1243
+ visibleRect = visibleRegion.boundingRect()
1244
+ # the visibleRect taking zoom into account
1245
+ scaledVisibleRect = QRect(int(visibleRect.x() / self.scale), int(visibleRect.y() / self.scale),
1246
+ int(visibleRect.width() / self.scale), int(visibleRect.height() / self.scale))
1247
+
1248
+ if self.image is None:
1249
+ # visibleRect can be empty on the very first paint (scroll area
1250
+ # hasn't finished layout yet), so always use self.rect() here.
1251
+ canvasPainter.eraseRect(self.rect())
1252
+ return
1253
+
1254
+ # try:
1255
+ # visibleRectF = QRectF(visibleRect)
1256
+ # scaledVisibleRectF = QRectF(scaledVisibleRect)
1257
+ # except:
1258
+ # pass
1259
+
1260
+ # print('testing in here, ', visibleRect, self.image,scaledVisibleRect)
1261
+ # print('testing in here, ', visibleRect, self.image is None, self.raw_image,scaledVisibleRect)
1262
+
1263
+ if isinstance(self.image, QImage):
1264
+ canvasPainter.drawImage(visibleRect, self.image, scaledVisibleRect)
1265
+ # else:
1266
+ # canvasPainter.drawImage(visibleRect, None, scaledVisibleRect)
1267
+ if not self.vdp.active and self.maskVisible and self.imageDraw is not None:
1268
+ canvasPainter.drawImage(visibleRect, self.imageDraw, scaledVisibleRect)
1269
+ # should draw the cursor
1270
+ canvasPainter.drawImage(visibleRect, self.cursor, scaledVisibleRect)
1271
+
1272
+ # ça ajoute un texte --> pas mal mais le refresh on scroll est pas top --> dois je le dessiner plutot dans vdp --> another set of objects that are non interactible and also cannot be saved and local???
1273
+
1274
+ # if True:
1275
+ # print('adding text')
1276
+ # myQPen = QPen(QtCore.Qt.blue, 3)
1277
+ # canvasPainter.setPen(myQPen)
1278
+ # canvasPainter.setPen(QColor("Green"))
1279
+ # canvasPainter.setFont(QFont('Helvetica', 48))
1280
+ # # print('drawing text')
1281
+ # canvasPainter.drawText(visibleRect.x(), visibleRect.y() +10, 'this is a test of your system')
1282
+ # # pb here is that the images is not cleared --> creates bugs
1283
+
1284
+ if self.vdp.active:
1285
+ self.vdp.paintEvent(canvasPainter, scaledVisibleRect)
1286
+ except:
1287
+ traceback.print_exc()
1288
+ finally:
1289
+ canvasPainter.end()
1290
+
1291
+ if __name__ == '__main__':
1292
+ # TODO add a main method so it can be called directly
1293
+ # maybe just show a canvas and give it interesting props --> TODO --> really need fix that too!!!
1294
+ import sys
1295
+ from qtpy.QtWidgets import QApplication
1296
+ from matplotlib import pyplot as plt
1297
+
1298
+ # should probably have his own scroll bar embedded somewhere
1299
+
1300
+ app = QApplication(sys.argv)
1301
+
1302
+
1303
+ # in fact that is easy to override methods before construction --> can easily do that in fact
1304
+
1305
+ # quite easy todo in fact
1306
+ class overriding_apply(Createpaintwidget):
1307
+ def apply(self):
1308
+ plt.imshow(self.get_mask())
1309
+ plt.show()
1310
+
1311
+ #Do not use show() in a GUI application.
1312
+ # If you have a FigureCanvas instance embedded in your app, call its
1313
+ # draw() method.
1314
+ # If you use pyplot.figure() to create a matplotlib window from your
1315
+ # app, call pyplot.draw().
1316
+
1317
+ # maybe add a figure canvas the to my code and plot to it and draw it --> quite easy and nice plotting capabilities then in python
1318
+
1319
+
1320
+
1321
+ # w = Createpaintwidget(enable_shortcuts=True)
1322
+ w = overriding_apply(enable_shortcuts=True)
1323
+ # w.set_image('/E/Sample_images/sample_images_PA/mini (copie)/focused_Series012.png')
1324
+ # w.set_image('/E/Sample_images/sample_images_PA/mini/focused_Series012.png')
1325
+ # w.set_image('C:/Users/aigouy/OneDrive/Desktop/test_images/sample_images_PA/test.png')
1326
+ w.set_image('C:/Users/aigouy/OneDrive/Desktop/Stack_best.tif')
1327
+ # w.set_image('/E/Sample_images/wings/fluorescent_wings_spots_charroux/909dsRed/0.tif')
1328
+ # w.set_image('/E/Sample_images/wings/fluorescent_wings_spots_charroux/contoles_test_X1VK06/0.tif')
1329
+ # w.set_mask('/E/Sample_images/sample_images_PA/mini (copie)/focused_Series012/handCorrection.png')
1330
+ # w.set_image(Img('/E/Sample_images/sample_images_PA/mini (copie)/focused_Series012.png'))
1331
+
1332
+ # print(tst.max())
1333
+ # tst = tst[..., np.newaxis]
1334
+
1335
+
1336
+
1337
+
1338
+ # tst = Img('/E/Sample_images/wings/fluorescent_wings_spots_charroux/909dsRed/0.tif')
1339
+ # tst = Img('/E/Sample_images/wings/fluorescent_wings_spots_charroux/contoles_test_X1VK06/0.tif')
1340
+ # tst = Img('/E/Sample_images/sample_images_PA/mini (copie)/focused_Series012.png')
1341
+ # tst = normalization(tst, method='Rescaling (min-max normalization)', range=[0,1], individual_channels=len(tst.shape)>2, clip=True)
1342
+ # tst = normalization(tst, method='Rescaling (min-max normalization)', range=[0,1], individual_channels=len(tst.shape)>2, clip=True)
1343
+ # tst = normalization(tst, method='Standardization', range=[0,1], individual_channels=True, clip=True)
1344
+ # tst = normalization(tst, method='Percentile', range=[0,1], individual_channels=len(tst.shape)>2, clip=True, normalization_minima_and_maxima=[1.,50.0])
1345
+ # tst = np.squeeze(tst)
1346
+
1347
+ # tst = auto_threshold(tst)
1348
+ # save_as_tiff(tst, output_name='/E/Sample_images/sample_images_PA/mini (copie)/tst_norm.tif')
1349
+
1350
+ # do I have a standar
1351
+ # tst = normalization(tst, method='standardization', range=[0,1], individual_channels=True)
1352
+ # print(tst.max())
1353
+ # w.set_image(tst)
1354
+
1355
+ # all is so easy to do this way
1356
+
1357
+
1358
+ # ça marche --> tres facile a tester
1359
+ # --> how can I do a zoom
1360
+ # ask whether self scroll or not --> if scrollable
1361
+
1362
+ # mask = w.get_mask()
1363
+ #
1364
+ # plt.imshow(mask)
1365
+ # plt.show()
1366
+
1367
+ w.show()
1368
+ sys.exit(app.exec_())
1369
+