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