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,3157 @@
1
+ from batoolset.settings.global_settings import set_UI # set the UI to qtpy
2
+ set_UI()
3
+ from batoolset.lists.tools import bool_list_to_index_list
4
+ from batoolset.luts.lut_minimal_test import PaletteCreator, list_available_luts, apply_lut, lsm_LUT_to_numpy
5
+ from batoolset.files.tools import smart_name_parser
6
+ from batoolset.files.tools import get_consolidated_filename_from_parent
7
+ from batoolset.strings.tools import reload_string_list, is_string_a_list_string, find_letter_before_h
8
+ from batoolset.drawings.shapes.automated_alignment_drawing_refactored import compute_translation_corrections
9
+ from batoolset.pyqts.tools import check_antialiasing, copy_transform_without_rotation, rotate_image, copy_scale_only, \
10
+ copy_scale_and_translation_only, crop_image, get_shape_after_rotation_and_crop, tst_translation
11
+ from batoolset.serializations.tools import create_objects_from_dict, object_to_xml
12
+ from batoolset.matplolibs.tools import get_fig_rect
13
+ import os
14
+ import traceback
15
+ from batoolset.drawings.shapes.Position import groupby_position, Position
16
+ from batoolset.drawings.shapes.rectangle2d import Rectangle2D
17
+ import numpy as np
18
+ import matplotlib.pyplot as plt
19
+ from qtpy import QtGui
20
+ from qtpy.QtGui import QPainter, QColor, QBrush, QPen, QTransform, QFontMetrics,QImage,QPixmap, QPolygonF
21
+ from qtpy.QtCore import Qt
22
+ from qtpy.QtSvg import QSvgGenerator
23
+ from qtpy.QtSvg import QSvgRenderer
24
+ from batoolset.drawings.shapes.line2d import Line2D
25
+ from batoolset.drawings.shapes.point2d import Point2D
26
+ from batoolset.figure.alignment import alignRight, alignLeft, alignTop, alignBottom, alignCenterH, alignCenterV, packY, \
27
+ packX, packYreverse, pack2, packing_modes
28
+ from batoolset.drawings.shapes.scalebar import ScaleBar
29
+ from batoolset.drawings.shapes.txt2d import TAText2D
30
+
31
+ from batoolset.img import Img, toQimage, RGB_to_BGR, guess_dimensions, max_proj, min_proj, avg_proj
32
+ from qtpy.QtCore import QRectF, QPointF, QSize, QRect, QByteArray
33
+ from qtpy.QtWidgets import QApplication
34
+ import sys
35
+ import io
36
+ from batoolset.tools.logger import TA_logger
37
+ import ast
38
+ import copy
39
+ import re
40
+
41
+ logger = TA_logger()
42
+
43
+
44
+ __DEBUG_NORMALIZATION__ = False
45
+
46
+
47
+ def _flat_level(value, dtype):
48
+ """The uint8 grey of a uniform image (min == max, nothing to stretch): its
49
+ value on the dtype's own scale. A flat image was drawn black whatever its
50
+ value - a white 255 background came out black."""
51
+ dtype = np.dtype(dtype)
52
+ v = float(value)
53
+ if not np.isfinite(v):
54
+ return np.uint8(0)
55
+ if dtype == np.bool_:
56
+ return np.uint8(255 if v else 0)
57
+ if dtype.kind in 'ui':
58
+ info = np.iinfo(dtype)
59
+ if dtype.kind == 'i':
60
+ return np.uint8(round(np.clip(v, 0, info.max) / info.max * 255))
61
+ return np.uint8(round(v / info.max * 255))
62
+ if 0 <= v <= 1: # float images are usually 0-1
63
+ return np.uint8(round(v * 255))
64
+ return np.uint8(np.clip(round(v), 0, 255))
65
+
66
+
67
+ def _normalize_to_uint8(arr, mn, mx):
68
+ """Single-pass normalization of arr to uint8 [0,255] for the display range (mn, mx).
69
+ Integer images go through a lookup table so no full-size float temporary is ever
70
+ allocated; other dtypes use one float32 pass (instead of the old float64 chain)."""
71
+ # do the scale math in Python floats: mn/mx arrive as numpy scalars in the image
72
+ # dtype and e.g. 255.0/(float16 range) is computed in float16 by numpy>=2, which
73
+ # overflows to inf and turns the whole output into NaN
74
+ mn = float(mn)
75
+ mx = float(mx)
76
+ if not (np.isfinite(mn) and np.isfinite(mx)) and arr.dtype.kind == 'f':
77
+ # NaN / ±inf pixels (masked or saturated float data) made the range
78
+ # NaN or infinite and the whole image black: use the finite pixels
79
+ finite = arr[np.isfinite(arr)]
80
+ if finite.size:
81
+ if not np.isfinite(mn):
82
+ mn = float(finite.min())
83
+ if not np.isfinite(mx):
84
+ mx = float(finite.max())
85
+ if not (np.isfinite(mn) and np.isfinite(mx)) or mx <= mn:
86
+ return np.zeros(arr.shape, dtype=np.uint8)
87
+ scale = 255.0 / (mx - mn)
88
+ if not np.isfinite(scale): # subnormal range: 255/(mx-mn) can overflow even float64
89
+ return np.zeros(arr.shape, dtype=np.uint8)
90
+ if arr.dtype == np.uint8:
91
+ lut = np.clip(np.rint((np.arange(256, dtype=np.float32) - np.float32(mn)) * np.float32(scale)), 0, 255).astype(np.uint8)
92
+ return lut[arr]
93
+ if arr.dtype == np.uint16:
94
+ lut = np.clip(np.rint((np.arange(65536, dtype=np.float32) - np.float32(mn)) * np.float32(scale)), 0, 255).astype(np.uint8)
95
+ return lut[arr]
96
+ # float pass: float32 unless the input dtype or the display range doesn't fit in it
97
+ # (float64 images with extreme values, or scale > float32 max for tiny ranges)
98
+ f32max = float(np.finfo(np.float32).max)
99
+ if (arr.dtype.kind == 'f' and arr.dtype.itemsize > 4) or abs(mn) > f32max or abs(mx) > f32max or scale > f32max:
100
+ compute = np.float64
101
+ else:
102
+ compute = np.float32
103
+ out = arr.astype(compute) # astype copies, so the in-place ops below are safe
104
+ # bound to the display range BEFORE scaling so the multiply cannot overflow
105
+ # (hot pixels / inf values in the image); NaNs pass through clip untouched
106
+ np.clip(out, mn, mx, out=out)
107
+ out -= compute(mn)
108
+ out *= compute(scale)
109
+ # NaN pixels (possible in float images) would make the uint8 cast warn
110
+ np.nan_to_num(out, copy=False, nan=0.0)
111
+ # round instead of truncating: with an inexact scale the max pixel otherwise
112
+ # lands on 254.999... and casts to 254 instead of 255
113
+ np.rint(out, out=out)
114
+ return out.astype(np.uint8)
115
+
116
+ from batoolset.matplolibs.tools import best_helvetica_clone as _best_helvetica_clone
117
+
118
+
119
+ # Vector files shown as panels (kept as vectors in PDF/SVG exports). A PDF page
120
+ # is converted to SVG with PyMuPDF (AGPL, optional) or, without it, rendered
121
+ # as an image with pypdfium2 (Apache/BSD) and wrapped in an SVG.
122
+ VECTOR_FILE_EXTS = ('.svg', '.pdf')
123
+
124
+ # resolution of a PDF page rendered without PyMuPDF, capped in total pixels
125
+ PDF_RASTER_DPI = 600
126
+ PDF_RASTER_MAX_PIXELS = 25_000_000
127
+
128
+
129
+ def is_vector_file(path):
130
+ return isinstance(path, str) and path.lower().endswith(VECTOR_FILE_EXTS)
131
+
132
+
133
+ def pdf_to_svg_bytes(path, page=0):
134
+ """The *page* (0-based) of the PDF at *path* as SVG bytes. With PyMuPDF
135
+ the page stays vector (text as outlines, images embedded at their own
136
+ resolution); otherwise pypdfium2 renders it at PDF_RASTER_DPI."""
137
+ try:
138
+ import fitz
139
+ except ImportError:
140
+ return _pdf_page_as_raster_svg(path, page)
141
+ with fitz.open(path) as doc:
142
+ if doc.page_count == 0:
143
+ raise ValueError(f'{path} has no pages')
144
+ svg = doc[min(max(0, int(page)), doc.page_count - 1)].get_svg_image(text_as_path=True)
145
+ return svg.encode('utf-8')
146
+
147
+
148
+ def _pdf_page_as_raster_svg(path, page=0):
149
+ """The PDF page rendered with pypdfium2, as an SVG of the page's size (in
150
+ points) holding one PNG image."""
151
+ try:
152
+ import pypdfium2
153
+ except ImportError as e:
154
+ raise ImportError('PDF panels need pypdfium2 (pip install pypdfium2) or, to keep '
155
+ 'them vector, PyMuPDF (pip install pymupdf)') from e
156
+ import base64
157
+ pdf = pypdfium2.PdfDocument(path)
158
+ try:
159
+ if len(pdf) == 0:
160
+ raise ValueError(f'{path} has no pages')
161
+ pg = pdf[min(max(0, int(page)), len(pdf) - 1)]
162
+ try:
163
+ w_pt, h_pt = pg.get_size()
164
+ scale = PDF_RASTER_DPI / 72.0
165
+ if w_pt * h_pt * scale * scale > PDF_RASTER_MAX_PIXELS:
166
+ scale = (PDF_RASTER_MAX_PIXELS / (w_pt * h_pt)) ** 0.5
167
+ bitmap = pg.render(scale=scale)
168
+ pil = bitmap.to_pil().convert('RGB')
169
+ bitmap.close()
170
+ finally:
171
+ pg.close()
172
+ finally:
173
+ pdf.close()
174
+ buf = io.BytesIO()
175
+ pil.save(buf, format='PNG')
176
+ data = base64.b64encode(buf.getvalue()).decode('ascii')
177
+ svg = (f'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" '
178
+ f'width="{w_pt:g}" height="{h_pt:g}" viewBox="0 0 {w_pt:g} {h_pt:g}">'
179
+ f'<image x="0" y="0" width="{w_pt:g}" height="{h_pt:g}" '
180
+ f'xlink:href="data:image/png;base64,{data}"/></svg>')
181
+ return svg.encode('utf-8')
182
+
183
+
184
+ def _make_svg_renderer(source):
185
+ """QSvgRenderer that keeps the SVG's aspect ratio when drawn into a panel
186
+ rectangle. Without this, a figure whose (tight-cropped) SVG aspect differs
187
+ from the panel rect — e.g. an equal-aspect image in a 4:3 matplotlib figure,
188
+ or a chart re-rendered with publication fonts at export — is silently
189
+ stretched, which distorts image data.
190
+
191
+ SVGs with clip paths (Inkscape drawings, Matplotlib's clipped axes) get a
192
+ ClippingSvgRenderer: Qt's own renderer ignores clip-path."""
193
+ r = None
194
+ try:
195
+ data = source
196
+ if isinstance(source, str):
197
+ with open(source, 'rb') as _f:
198
+ data = _f.read()
199
+ elif isinstance(source, QByteArray):
200
+ data = bytes(source)
201
+ if isinstance(data, (bytes, bytearray)):
202
+ from batoolset.drawings.svg_clip_renderer import ClippingSvgRenderer
203
+ if ClippingSvgRenderer.needed(bytes(data)):
204
+ r = ClippingSvgRenderer(bytes(data))
205
+ except Exception:
206
+ r = None
207
+ if r is None:
208
+ r = QSvgRenderer(source)
209
+ try:
210
+ r.setAspectRatioMode(Qt.AspectRatioMode.KeepAspectRatio)
211
+ except AttributeError: # Qt < 5.15
212
+ pass
213
+ # the SVG source itself, embedded as is in SVG exports (svg_embed)
214
+ try:
215
+ if isinstance(data, (bytes, bytearray)):
216
+ r._pyf_svg_source = bytes(data)
217
+ except Exception:
218
+ pass
219
+ return r
220
+
221
+
222
+ # gap (figure units = px at 100 %) between a panel's edge and the letters,
223
+ # labels and scale bars pinned to it
224
+ EDGE_MARGIN = 2
225
+
226
+
227
+ class Image2D(Rectangle2D):
228
+ # When True, skip pixel loading in __init__ (used for non-blocking DND deserialization).
229
+ # Call load_img() later on each object to load pixel data.
230
+ _lazy_mode = False
231
+ # When True, a scripted panel that already has a stored width/height keeps
232
+ # that rectangle and its custom_loading_script is NOT executed. Used by the
233
+ # headless layout recompute so re-laying out a figure never runs (possibly
234
+ # untrusted, possibly slow) panel code; the script itself is preserved.
235
+ _skip_sized_scripts = False
236
+ # When True (editable PowerPoint / Illustrator export), Matplotlib plots are
237
+ # exported with their text as real text (svg.fonttype 'none') instead of
238
+ # glyph outlines, so tick labels and axis titles stay editable there.
239
+ _export_editable_text = False
240
+
241
+ # min = None, max = None,
242
+ def __init__(self, *args, x=None, y=None, width=None, height=None, data=None, dimensions=None, channels=None, LUTs=None, projection=None, projection_z_start=None, projection_z_end=None, normalization=None, filename=None, isText=False, fraction_of_parent_image_width_if_image_is_inset=0.25, opacity=1., theta=None, crop_left=0, crop_right=0, crop_top=0, crop_bottom=0,fill_color=None, border_color=0xFFFFFF,border_size=None, placement=Position('Top-Right'),annotations=None, custom_loading_script=None, allow_unknown_files=True, scale=1, user_comments='',__version__=1.0, px_to_unit_conversion_factor=0, extra_user_translation=(0,0),script_log=None, template_rect=None, serie_to_open=None, publication_style=True, publication_font_size=12, publication_line_width=0.65, publication_font='auto', publication_svg_stroke_width=0.65, follow_parent_display=False, flip_h=False, flip_v=False, **kwargs):
243
+
244
+ # print('args',args)
245
+ # if kwargs:
246
+ # print('ignored parameters', kwargs)
247
+
248
+ if template_rect:
249
+ try:
250
+ # print('template_rect',template_rect)
251
+ pattern = r'\((.*?)\)'
252
+ match = re.search(pattern, template_rect)
253
+
254
+ if match:
255
+ values_str = match.group(1) # Get the matched substring
256
+ values = [float(x) for x in values_str.split(',')] # Convert to floats
257
+
258
+ # Create a QRectF object
259
+ template_rect = QRectF(*values)
260
+ # print(template_rect)
261
+ except Exception:
262
+ pass
263
+ else:
264
+ # print('no template rect')
265
+ pass
266
+
267
+ self.lower_indices = None
268
+ self.upper_indices = None
269
+ self.lower_values = None
270
+ self.upper_values = None
271
+ # self.min=min # will store the normalization min
272
+ # self.max=max # will store the normalization max # nB normalization is mandatory for 16bits images and probably recommended for LUTs -> see how I should handle that
273
+ self.projection = projection # to store if a max proj or alike should be made
274
+ self.projection_z_start = projection_z_start # first Z slice included in projection (None = 0)
275
+ self.projection_z_end = projection_z_end # last Z slice (exclusive) in projection (None = all)
276
+ self.normalization = normalization # this will contain all the normalization infos as a dict
277
+ self.isText = isText
278
+ if isinstance(self.normalization, str):
279
+ try:
280
+ self.normalization = ast.literal_eval(self.normalization)
281
+ except Exception:
282
+ # JSON ('{"per_channel": true}') as older MCP calls stored it:
283
+ # left as a string the panel was drawn solid white
284
+ try:
285
+ import json
286
+ self.normalization = json.loads(self.normalization)
287
+ except Exception:
288
+ pass
289
+ if not isinstance(self.normalization, dict):
290
+ self.normalization = None
291
+ self.channels = channels # will store the channel state (active or not)
292
+ if isinstance(self.channels, str):
293
+ try:
294
+ self.channels = ast.literal_eval(self.channels)
295
+ except Exception:
296
+ pass
297
+
298
+ # print('inputed svchannels', self.channels)
299
+ # print('inputed svchannelsb', channels)
300
+
301
+ self.LUTs = LUTs # will store the user defined LUTs
302
+ if isinstance(self.LUTs, str):
303
+ try:
304
+ self.LUTs = ast.literal_eval(self.LUTs)
305
+ except Exception:
306
+ pass
307
+ self.dimensions = dimensions
308
+ if isinstance(self.dimensions, str):
309
+ try:
310
+ self.dimensions = ast.literal_eval(self.dimensions)
311
+ except Exception:
312
+ pass
313
+
314
+
315
+
316
+
317
+ self.script_log = script_log # a log of the screen so that the user can see and read the errors of their script -−> can be very useful for debug
318
+ self.extra_user_translation = extra_user_translation
319
+ self.px_to_unit_conversion_factor=px_to_unit_conversion_factor
320
+ self.user_comments = user_comments # contains some useful hints for the person that made the figure
321
+ # if this stuff is set then allow to load the image using that -−> TODO
322
+ self.qimage = None
323
+ self.custom_loading_script = custom_loading_script # TODO implement that --> should assign to self.img smthg that I can handle -−> either a raster or a svg or whatever # par exemple pr loader les certains frames d'une image -−> qq chose de facile à faire
324
+ self.publication_style = publication_style
325
+ self.publication_font_size = publication_font_size
326
+ self.publication_line_width = publication_line_width
327
+ self.publication_font = _best_helvetica_clone() if publication_font == 'auto' else publication_font
328
+ self.publication_svg_stroke_width = publication_svg_stroke_width
329
+ self._raw_svg_bytes = None
330
+
331
+ # crops
332
+ self.crop_left = crop_left
333
+ self.crop_right = crop_right
334
+ self.crop_top = crop_top
335
+ self.crop_bottom = crop_bottom
336
+ self.img = None
337
+ self.serie_to_open = serie_to_open # index of the series to open (for multi-series files, e.g. .lif)
338
+ self.line_style = None # useless yet just for compat
339
+ if not annotations: # DEV NOTE KEEP WEIRD SERIALIZATION CODE IF I DONT DO THAT --> SO KEEP --> IF I INITITATE annotations = [] in the init then nested images in annotations cause an infinite loop in serialization, no clue why though
340
+ annotations = []
341
+ if isinstance(annotations, dict): # deprecated --> maybe remove that because it was successfully changed
342
+ annotations=create_objects_from_dict(annotations)
343
+ elif isinstance(annotations, list) and len(annotations)>0:
344
+ if isinstance(annotations[0], dict):
345
+ annotations = create_objects_from_dict(annotations)
346
+ # an empty <annotations/> in a .pyf is read back as '' (or '[]', 'None'):
347
+ # adding a letter then called ''.insert (seen after deleting a panel's
348
+ # last annotation and reopening)
349
+ if isinstance(annotations, str) and annotations.strip() in ('', '[]', 'None'):
350
+ annotations = []
351
+
352
+ self.annotations=annotations
353
+ if isinstance(placement, str):
354
+ placement = Position(placement)
355
+ self.placement=placement
356
+ self.renderer = None # replaced all of these by self.img
357
+
358
+ # if the image is inserted as an inset then draw it as a fraction of parent width
359
+ # inset parameters
360
+ self.fraction_of_parent_image_width_if_image_is_inset = fraction_of_parent_image_width_if_image_is_inset
361
+ # a zoom callout's magnified inset shows the parent's own pixels: it then
362
+ # takes the parent's display settings (LUTs, channels, …), see _sync_display_from_parent
363
+ if isinstance(follow_parent_display, str):
364
+ follow_parent_display = follow_parent_display.strip().lower() == 'true'
365
+ self.follow_parent_display = bool(follow_parent_display)
366
+ # mirroring is part of how the image is drawn (like theta): self.img keeps
367
+ # the original pixels and only the displayed qimage is flipped
368
+ self.flip_h = str(flip_h).strip().lower() == 'true' if isinstance(flip_h, str) else bool(flip_h)
369
+ self.flip_v = str(flip_v).strip().lower() == 'true' if isinstance(flip_v, str) else bool(flip_v)
370
+ self.border_size = border_size # no border by default
371
+ self.border_color = border_color # white border by default
372
+ self.filename = filename
373
+
374
+ if not args and filename:
375
+ args = [filename]
376
+
377
+ if args:
378
+ if isinstance(args[0], str):
379
+ self.filename = args[0]
380
+ elif isinstance(args[0], Img):
381
+ self.filename = None
382
+ self.img = args[0]
383
+ # self.qimage = toQimage(self.img)
384
+ self.update_qimage()
385
+ if x is None:
386
+ x = 0
387
+ if y is None:
388
+ y = 0
389
+
390
+ w,h = self.get_raw_size()
391
+ # try:
392
+ super(Image2D, self).__init__(x, y, w, h)
393
+ # except:
394
+ # super(Image2D, self).__init__(x, y, self.img.shape[1], self.img.shape[0])
395
+ elif isinstance(args[0], plt.Figure):
396
+ # the passed image is in fact a matplotlib plot --> init super with the corresponding QrectF
397
+ # super(Image2D, self).__init__(get_fig_rect(args[0]))
398
+ self.img = args[0]
399
+ w,h = self.get_raw_size()
400
+ super(Image2D, self).__init__(0,0,w,h)
401
+ self.setFigure(self.img)
402
+ elif isinstance(args[0], np.ndarray):
403
+ # the only pb of that is that it cannot be reloaded -−> rather recommend the user to use scripts
404
+ self.img=args[0]
405
+ # self.qimage = toQimage(self.img)
406
+ self.update_qimage()
407
+
408
+ w,h = self.get_raw_size()
409
+ # try:
410
+ # super(Image2D, self).__init__(0,0, self.img.get_width(), self.img.get_height())
411
+ super(Image2D, self).__init__(0,0, w, h)
412
+ # except:
413
+ # super(Image2D, self).__init__(0,0, self.img.shape[1], self.img.shape[0])
414
+
415
+ if x is None and y is None and width is not None and height is not None and self.filename is None:
416
+ super(Image2D, self).__init__(0, 0, width, height)
417
+ # self.img = QRectF(0, 0, width, height) # hack to fake an image... to allow crop and alike to always work
418
+ # if not template_rect:
419
+ # self.img = QRectF(0, 0, width, height) # hack to fake an image... to allow crop and alike to always work
420
+ # else:
421
+ # self.img = template_rect
422
+ elif self.filename is not None: # and not isinstance(self.filename, TAText2D):
423
+ # handle vector files (svg, pdf) first
424
+ if is_vector_file(self.filename):
425
+ super(Image2D, self).__init__(0, 0, 512, 512) # fake definition
426
+ self.setFigure(self.filename) #real loading of the parameters
427
+ else:
428
+ if Image2D._lazy_mode:
429
+ # Defer pixel loading; stored width/height (set below) keep layout correct
430
+ self.img = None
431
+ super(Image2D, self).__init__(0, 0, width or 512, height or 512)
432
+ else:
433
+ try:
434
+ self.img = Img(self.filename, serie_to_open=self.serie_to_open)
435
+ # self.qimage = toQimage(self.img)
436
+ self.update_qimage()
437
+ # width = self.qimage.width()
438
+ # height = self.qimage.height()
439
+
440
+ # print('width height defined here127', width, height, self.filename)
441
+ w,h = self.get_raw_size()
442
+ super(Image2D, self).__init__(0, 0, w, h)
443
+ except Exception:
444
+ if not self.custom_loading_script:
445
+ # if the file canot be opened and there is no script --> then there is chance that it works
446
+ if not self.filename == '@template@':
447
+ logger.error('could not load image '+str(self.filename))
448
+ if not allow_unknown_files:
449
+ return
450
+ else:
451
+ # define a default image with 512x512 and display error on top of it
452
+ # in fact this can be an empty image
453
+ self.img = None # this is the indication that an error occurred and that I cannot do much
454
+ super(Image2D, self).__init__(0, 0, 512, 512)
455
+ if x is not None:
456
+ self.setX(x)
457
+ if y is not None:
458
+ self.setY(y)
459
+ if width is not None:
460
+ self.setWidth(width)
461
+ if height is not None:
462
+ self.setHeight(height)
463
+ elif x is not None and y is not None and width is not None and height is not None and self.img is None:
464
+ self.img = None
465
+ super(Image2D, self).__init__(x, y, width, height)
466
+ elif self.filename is not None: # and not isinstance(self.filename, TAText2D):
467
+ if Image2D._lazy_mode:
468
+ self.img = None
469
+ super(Image2D, self).__init__(x or 0, y or 0, width or 512, height or 512)
470
+ else:
471
+ self.img = Img(self.filename, serie_to_open=self.serie_to_open)
472
+ # self.qimage = toQimage(self.img)
473
+ self.update_qimage()
474
+ if x is None:
475
+ x = 0
476
+ if y is None:
477
+ y = 0
478
+ w,h = self.get_raw_size()
479
+ # super(Image2D, self).__init__(x, y, self.img.get_width(), self.img.get_height())
480
+ super(Image2D, self).__init__(x, y, w,h)
481
+
482
+ # try a ultimate rescue if there was a script
483
+ if custom_loading_script and Image2D._skip_sized_scripts and width and height:
484
+ # layout-only construction: keep the stored rect, don't run the code
485
+ custom_loading_script = None
486
+ if custom_loading_script:
487
+
488
+ # why is image None there
489
+ self.execute_code_unsafe(custom_loading_script) # MEGA TODO --> maybe I need to log the script error and put it somewhere in a widget -−>
490
+ # print('entering custom loading script', self.img, self.script_log)
491
+ if self.script_log:
492
+ print('Script output:\n',self.script_log)
493
+ if custom_loading_script and self.img is not None:
494
+ # TODO --> avoid code duplication!!! --> TODO -> split my code into plenty of stuff
495
+ if isinstance(self.img, str):
496
+ # a vector file made by the script (e.g. latex(...)): shown like a
497
+ # dropped PDF/SVG, the panel keeps its own filename (re-run on load)
498
+ self._show_script_file(self.img)
499
+ elif isinstance(self.img, plt.Figure):
500
+ super(Image2D, self).__init__(get_fig_rect(self.img))
501
+ # keep the saved position: the figure rect starts at 0,0, so a
502
+ # scripted panel (plot) jumped to the origin when reopened
503
+ self.moveTo(x if x is not None else 0, y if y is not None else 0)
504
+ self.setFigure(self.img)
505
+ if isinstance(self.img, np.ndarray):
506
+ # print('setting new stuff here')
507
+ # self.qimage = toQimage(self.img)
508
+ self.update_qimage()
509
+ w, h = self.get_raw_size()
510
+ # self.setWidth(self.qimage.width())
511
+ # self.setHeight(self.qimage.height())
512
+ # self.setWidth(w)
513
+ # self.setHeight(h)
514
+ super(Image2D, self).__init__(x if x is not None else 0, y if y is not None else 0, w, h)
515
+ try:
516
+ self.__init_called__
517
+ except Exception:
518
+ if not allow_unknown_files:
519
+ raise Exception('Image not initialized properly!, super-class __init__() was never called')
520
+ else:
521
+ super(Image2D, self).__init__(0, 0, 512, 512) # create an error image maybe the custom code can be edited later?
522
+ # self.img=QRectF(0, 0, 512, 512)
523
+ if not template_rect:
524
+ self.img = QRectF(0, 0, 512,
525
+ 512) # hack to fake an image... to allow crop and alike to always work
526
+ else:
527
+ self.img = template_rect
528
+
529
+ # THIS SHOULD ALWAYS BE AT THE VERY END OTHERWISE SUPER OVERRIDES THE VALUES --> PROBABLY NEED TO DO IT IN A SMARTER WAY BUT OK FOR NOW !!!!
530
+ # the opacity given (it was always reset to 1: an opacity saved in a .pyf
531
+ # or set through the MCP server was lost on reload)
532
+ try:
533
+ self.opacity = min(1., max(0., float(opacity))) if opacity is not None else 1.
534
+ except (TypeError, ValueError):
535
+ self.opacity = 1.
536
+ self.scale = scale
537
+ self.translation = QPointF()
538
+ self.fill_color = fill_color # can be used to define a bg color for rotated images (and or transparent images...)
539
+ self.theta = theta
540
+
541
+
542
+ if self.is_template(): # if image is converted to template then we must remove the conversion factor (maybe make this an option some day but I think it makes sense)
543
+ self.px_to_unit_conversion_factor=0 # be sure to reset it if in a template
544
+
545
+ if not self.px_to_unit_conversion_factor:
546
+ # try get it from the image
547
+ self.get_px_to_unit_conversion_factor_from_metadata()
548
+
549
+
550
+
551
+ if template_rect:
552
+ if self.img is None or isinstance(self.img, QRectF):
553
+ self.img = template_rect
554
+ self.__version__ = __version__
555
+
556
+ def get_px_to_unit_conversion_factor_from_metadata(self):
557
+ try:
558
+ self.px_to_unit_conversion_factor = self.img.metadata['vx']
559
+ except Exception:
560
+ self.px_to_unit_conversion_factor = 0
561
+ # print()
562
+
563
+ def get_nb_channels(self):
564
+ if isinstance(self.img, np.ndarray):
565
+ guessed_dimensions =guess_dimensions(self.img)
566
+ if 'c' in guessed_dimensions:
567
+ return self.img.shape[guessed_dimensions.index('c')]
568
+ else:
569
+ return 1
570
+ else:
571
+ return 0
572
+
573
+ def get_extra_dimensions(self, handle_proj=False):
574
+ if not isinstance(self.img, np.ndarray):
575
+ return None
576
+ guessed_dimensions = guess_dimensions(self.img)
577
+ extra_dims = guessed_dimensions.replace('h', '').replace('w', '').replace('c', '')
578
+ if extra_dims and handle_proj:
579
+ if self.projection is not None:
580
+ extra_dims = extra_dims[:-1]
581
+ return extra_dims
582
+
583
+ def clip_by_frequency(self, lower_cutoff=None, upper_cutoff=0.05, channel_mode=True):
584
+ '''
585
+ Clips the values of an image based on frequency cutoffs.
586
+
587
+ Args:
588
+ img (ndarray): Input image.
589
+ lower_cutoff (float, optional): Lower frequency cutoff.
590
+ upper_cutoff (float, optional): Upper frequency cutoff.
591
+ channel_mode (bool, optional): Specifies whether to perform clipping on individual channels.
592
+
593
+ Returns:
594
+ ndarray: Clipped image.
595
+ '''
596
+
597
+ logger.debug(' inside clip ' + str(lower_cutoff) + str(upper_cutoff) + str(channel_mode))
598
+
599
+ # Check if all cutoff values are 0 or None, in which case the image remains unchanged.
600
+ if lower_cutoff == upper_cutoff == 0 or lower_cutoff == upper_cutoff == None:
601
+ logger.debug('clip: keep image unchanged')
602
+ return img
603
+
604
+ # Check if either lower_cutoff or upper_cutoff is None while the other is 0, in which case the image remains unchanged.
605
+ if (lower_cutoff is None and upper_cutoff == 0) or (upper_cutoff is None and lower_cutoff == 0):
606
+ logger.debug('clip: keep image unchanged')
607
+ return img
608
+
609
+ logger.debug('chan mode ' + str(channel_mode))
610
+
611
+ # If the maximum and minimum values of the image are the same, return the image as is.
612
+ if img.max() == img.min():
613
+ return img
614
+
615
+ # If channel_mode is True, perform clipping on individual channels.
616
+ if channel_mode:
617
+ for ch in range(img.shape[-1]):
618
+ img[..., ch] = self.clip_by_frequency(img[..., ch], lower_cutoff=lower_cutoff, upper_cutoff=upper_cutoff,
619
+ channel_mode=False)
620
+ return img
621
+
622
+ logger.debug('Removing image outliers/hot pixels')
623
+
624
+ # Compute the maximum value for clipping based on the upper_cutoff.
625
+ if upper_cutoff is not None:
626
+ max = np.percentile(img, 100. * (1. - upper_cutoff))
627
+ img[img > max] = max
628
+
629
+ # Compute the minimum value for clipping based on the lower_cutoff.
630
+ if lower_cutoff is not None:
631
+ min = np.percentile(img, 100. * lower_cutoff)
632
+ img[img < min] = min
633
+
634
+ return img
635
+
636
+ def load_img(self):
637
+ """Load pixel data now (used after lazy-mode deserialization)."""
638
+ if self.filename is None or self.img is not None:
639
+ return
640
+ try:
641
+ self.img = Img(self.filename, serie_to_open=getattr(self, 'serie_to_open', None))
642
+ self.update_qimage()
643
+ except Exception:
644
+ if not self.custom_loading_script:
645
+ if self.filename != '@template@':
646
+ logger.error('could not load image ' + str(self.filename))
647
+ self.img = None
648
+
649
+ def update_qimage(self, reset_outlier_pos=False):
650
+ # MEGA TODO NB SOME OF THE PARAMTERS PROBABLY NEED NOT BE CHANGED UNTIL IMAGE IS CHANGED --> SPLIT THIS IN INIT_IMAGE AND UPDATE_IMAGE AND CALL THE APPROPRIATE for example the bins/histo/min/max and so on need not be redone
651
+
652
+ # TODO --> merge all so that I do not have to create yet another layer ??? -−> TODO
653
+ # shall I use views to avoid memory overload !!!
654
+ # TODO --> try to finalize all!!!
655
+
656
+ # print('%%%'*32)
657
+ # print('neo parameters', self.normalization, self.projection)
658
+ # self.channels = 1
659
+
660
+ # if reset_outlier_pos:
661
+ # self.remove_outliers()
662
+ # self.reset_outliers()
663
+
664
+ # print('MEGA DEBUGGING',self.img.max())
665
+ if __DEBUG_NORMALIZATION__:
666
+ print('#'*20+'ENTERING UPDATE QIMAGE'+'#'*20)
667
+
668
+ if not isinstance(self.img, np.ndarray):
669
+ self.qimage = None
670
+ self._update_display_proxy() # clears proxy + display cache
671
+ # now I need to get the max and min per channel maybe if per channel is on
672
+ return
673
+
674
+ if True:
675
+ # print('I am inside')
676
+ per_channel = False
677
+ if self.normalization is not None:
678
+ if __DEBUG_NORMALIZATION__:
679
+ print('called')
680
+ if self.normalization is not None:
681
+ if __DEBUG_NORMALIZATION__:
682
+ print('called 2')
683
+ if 'per_channel' in self.normalization:
684
+ per_channel = self.normalization['per_channel']
685
+
686
+ if __DEBUG_NORMALIZATION__:
687
+ print('called 3')
688
+ if 'ignore_hot_spots' in self.normalization and self.normalization['ignore_hot_spots'] is not None and self.normalization['ignore_hot_spots'] is not False:
689
+ # print('TODO -−> NOW I NEED TO CHANGE THE NORMALIZATION -−> ACTION REQUIRED')
690
+ # # pass
691
+ if __DEBUG_NORMALIZATION__:
692
+ print('called 4')
693
+ # # self.reset_outliers()
694
+ # print('called 5')
695
+ #
696
+ # print('removing outliers')
697
+
698
+ self.remove_outliers(per_channel=per_channel)
699
+ else:
700
+ if __DEBUG_NORMALIZATION__:
701
+ print('called 6')
702
+ pass
703
+ # self.restore_outliers()
704
+ # self.reset_outliers()
705
+ else:
706
+ if __DEBUG_NORMALIZATION__:
707
+ print('called 7')
708
+ pass
709
+ # self.restore_outliers()
710
+ # self.reset_outliers()
711
+
712
+ # print('MEGA DEBUGGING3', self.img.max())
713
+
714
+
715
+ # maybe I could integrate in there the rotation (maybe also crop) and all alike stuff and max proj and also the
716
+ image_to_display = self.img
717
+
718
+
719
+
720
+
721
+ target_dims = guess_dimensions(self.img)
722
+ # original_dims=target_dims
723
+
724
+ # print('super init target_dims',target_dims)
725
+ proj_letter = find_letter_before_h(target_dims)
726
+
727
+
728
+
729
+ # parse the image if needed
730
+ if isinstance(self.dimensions, dict):
731
+ # print('checking order',self.dimensions['order'])
732
+ # print('checking values',self.dimensions['values'])
733
+ if len(self.dimensions['order'])!=len(self.img.shape) or target_dims!=self.dimensions['order']:
734
+ print('dimension mismatch between dimensions and current image -−> ignoring')
735
+ else:
736
+ for ddd,dim_to_chose in enumerate(self.dimensions['values']):
737
+ if target_dims[ddd] == proj_letter and self.projection: # we keep the letter for projection if projection is selected
738
+ # if there is a dim dimension and the user wants a projection --> do not allow dimension reduction!!!
739
+ continue
740
+ if dim_to_chose>=0 and dim_to_chose<self.img.shape[ddd]:
741
+ image_to_display = image_to_display[dim_to_chose]
742
+ else:
743
+ # print('invalid dimension specified assuming image has changed and resetting the value')
744
+ image_to_display = image_to_display[0]
745
+
746
+ if len(target_dims) > len(image_to_display.shape):
747
+ target_dims = target_dims[len(target_dims)-len(image_to_display.shape):]
748
+
749
+ # print('final rendering',image_to_display.shape)
750
+ # print('checking img', image_to_display.shape, 'vs', self.img.shape)
751
+ # if self.channels is not None and not self.channels == 'merge' and 'c' in target_dims:
752
+ # if self.channels>=0 and self.channels < self.img.shape[-1]:
753
+ # image_to_display=image_to_display[...,self.channels]
754
+ # # image_to_display=image_to_display[...,np.newaxis] # do this to avoid losing dimension
755
+ # target_dims = target_dims.replace('c','')
756
+
757
+ # print('user defined Luts here', self.LUTs)
758
+
759
+ # print('checking img', image_to_display.shape)
760
+ # shall I allow LUTS for non channels images −−> if so add them a channel before that -−> but then need add a dimension
761
+
762
+ if False:
763
+ # try add a channel but somehow that creates a bug --> probably do not use that!!!
764
+ if not 'c' in target_dims:
765
+ image_to_display = image_to_display[..., np.newaxis]
766
+ target_dims+='c'
767
+
768
+ if self.channels and 'c' in target_dims:
769
+ # print('inside channels sel self.channels', self.channels)
770
+ list_of_true_indices = bool_list_to_index_list(self.channels)
771
+ # print('list_of_true_indices',list_of_true_indices)
772
+ if list_of_true_indices:
773
+ image_to_display=image_to_display[...,list_of_true_indices] # that is funny the behaviour is different as a list keeps the last channel even if unique whererad the int removes it!!!!
774
+ else:
775
+ list_of_true_indices = [i for i,_ in enumerate(self.channels)] # if no index --> force select all for the LUTs --> make sens
776
+ # print('list_of_true_indices fixed', list_of_true_indices)
777
+ if image_to_display.shape[-1]==1:
778
+ target_dims = target_dims.replace('c', '') # force it to be gray # so why is it colored this time ???
779
+ image_to_display=image_to_display[...,0] # needed to remove the channel and the color # see how I can handle the LUts naow and do a serializable LUT object
780
+ # print('removing c')
781
+
782
+
783
+ # print('image_to_display after channel selection',image_to_display.shape)
784
+
785
+ luts = None
786
+ # if 'c'in original_dims:
787
+ if True:
788
+ if __DEBUG_NORMALIZATION__:
789
+ print('entering Lut1')
790
+ # keep existing LUTs if they exist !!!
791
+ try:
792
+ if hasattr(self.img,'metadata') and 'LUTs' in self.img.metadata:
793
+ if self.img.metadata['LUTs']:
794
+ # print('checking luts',self.img.metadata['LUTs'])
795
+ # print('checking luts',len(self.img.metadata['LUTs']))
796
+ luts=self.img.metadata['LUTs']
797
+ # print(type(luts[0]))
798
+ # print('shape lut', luts[0].shape) # (3, 256)
799
+ # Use a list comprehension to select only the specified elements
800
+ if self.channels:
801
+ # luts = [luts[i] if channel_state else for i, channel_state in enumerate(self.channels)] # if there are LUTs they are applied --> I could also swap these LUts for the user chosen ones if any
802
+ # tmp = []
803
+ # for sss, state in enumerate(self.channels):
804
+ # if state:
805
+ # tmp.append(luts[sss])
806
+ # luts=tmp
807
+ tmp = [lut for lut, state in zip(luts, self.channels) if state] # we only get the luts for active states
808
+ if not tmp:
809
+ pass
810
+ else:
811
+ luts=tmp
812
+ else:
813
+ luts = luts
814
+ # I would need a LUT button and also to serialize the LUts --> maybe make an object
815
+
816
+ # palette = None
817
+ # if True:
818
+ # # lut = self.lut_combo.currentText()
819
+ # lutcreator = PaletteCreator()
820
+ # luts_lst = lutcreator.list
821
+ # # lut = lutcreator.create3(luts[lut])
822
+ # try:
823
+ # # palette = lutcreator.create3(luts[0])
824
+ # palette = lutcreator.create3(luts_lst["RAINBOW_1"])# if LUT is from a string --> try save that, otherwise serialize the whole array in the sortest possible way
825
+ # # palette = lutcreator.create3("GRAY") #
826
+ # except:
827
+ #
828
+ # # palette = None
829
+ # # default to grey palette
830
+ # palette = lutcreator.create3(luts_lst['GRAY'])
831
+ #
832
+ # print('shape lut', luts[0].shape)
833
+ # print('palette',palette)
834
+ # if palette is not None:
835
+ # # ça marche je peux facilement remplacer une lut par ça en fait --> à faire -->
836
+ # palette = np.asarray(palette)
837
+ # print('shape pal', palette.shape)
838
+ # luts[0]=palette
839
+ except Exception:
840
+ traceback.print_exc()
841
+ # I need to update the dims ???
842
+
843
+ # print('bobobob')
844
+ # print('self.LUTs',self.LUTs, 'fixed LUTs', len(luts) if luts else luts) #--> ok
845
+ # print('self.img.shape[-1]',self.img.shape[-1]) #--> ok
846
+
847
+ final_luts_to_apply = []
848
+ if luts:
849
+ final_luts_to_apply.extend(luts)
850
+ luts2 = []
851
+ if self.LUTs and self.img.shape[-1]==len(self.LUTs): # TODO --> check that
852
+ if __DEBUG_NORMALIZATION__:
853
+ print('entering Lut2')
854
+
855
+ # keep only the LUTs of the specified user channels
856
+ if self.channels:
857
+ # luts2 = [self.LUTs[i] for i, channel_state in enumerate(self.channels)] # if there are LUTs they are applied --> I could also swap these LUts for the user chosen ones if any
858
+ # luts2 = [self.LUTs[i] for lut, state in zip(luts, self.channels) if state] # we only get the luts for active states
859
+ luts2 = []
860
+ for i,state in enumerate(self.channels):
861
+ if state:
862
+ luts2.append(self.LUTs[i])
863
+ if not luts2: # nothing selected is the same as all is selected -−> fix all
864
+ luts2=self.LUTs
865
+ else:
866
+ luts2 = self.LUTs
867
+
868
+
869
+ if __DEBUG_NORMALIZATION__:
870
+ print('checking luts2', luts2)
871
+ # print('checking luts2', luts2) # vide
872
+ # print('checking final_luts_to_apply', len(final_luts_to_apply))
873
+ if luts2:
874
+ if not final_luts_to_apply:
875
+ final_luts_to_apply.extend(luts2)
876
+ else:
877
+ for l in range(len(final_luts_to_apply)):
878
+ final_luts_to_apply[l]=final_luts_to_apply[l] if luts2[l]==None else luts2[l]
879
+
880
+
881
+
882
+ luts=final_luts_to_apply
883
+ # print('###########')
884
+
885
+ # no in fact max and min should be obtained there !!!!
886
+
887
+ # print('checking img after', image_to_display.shape)
888
+ #
889
+ # print('target_dims',target_dims)
890
+ #
891
+ # print(len(target_dims), len(image_to_display.shape))
892
+
893
+ # TODO --> hack qimage so that if more than 3 channels and blend I blend each channel and then compute the sum or avg or max image of all the colors see how imageJ is doing and do the same!!!
894
+ # check my LUTs and finalize that!!!
895
+ self.qimage = None
896
+
897
+ # print('dabouyo')
898
+
899
+ # the real global max is there in fact -−> still I do not get it ???
900
+
901
+ # print('megrabouya', self.channels)
902
+ if __DEBUG_NORMALIZATION__:
903
+ print('megrabouya', self.channels)
904
+
905
+ # print('calling the prepare_image_for_qimage')
906
+ try:
907
+ self.prepare_image_for_qimage(image_to_display, target_dims, luts)
908
+ except Exception:
909
+ traceback.print_exc()
910
+
911
+ if self.qimage is None:
912
+ # if __DEBUG_NORMALIZATION__:
913
+ print('failed --> ROLLING BACK')
914
+ # print('fixed target_dims', target_dims, image_to_display.shape)
915
+
916
+ try:
917
+ self.qimage = toQimage(image_to_display, normalize=False, dimensions=target_dims, luts=luts)
918
+ except Exception:
919
+ traceback.print_exc()
920
+ self.qimage = None
921
+ if self.qimage is None:
922
+ # said in the words the exporters look for ('could not load
923
+ # image'): the panel was exported empty and the export said OK
924
+ logger.error('could not load image ' + str(self.filename))
925
+ # self.qimage = toQimage(image_to_display, dimensions=target_dims, luts=None)
926
+
927
+ if self.qimage is not None and (getattr(self, 'flip_h', False) or getattr(self, 'flip_v', False)):
928
+ self.qimage = self.qimage.mirrored(bool(self.flip_h), bool(self.flip_v))
929
+
930
+ if __DEBUG_NORMALIZATION__:
931
+ print('~'*30)
932
+
933
+ # print('restoring and resetting outliers',self.img.max())
934
+ self.restore_outliers()
935
+ self.reset_outliers()
936
+
937
+ self._update_display_proxy() # rebuild the interactive downsampled proxy (huge images only)
938
+ # print(self.img.max())
939
+ # restore is ok but display is not
940
+
941
+ # print('~' * 30)
942
+
943
+ del image_to_display
944
+ # del self.img # that would be great but then I would need tricks to know its parameters
945
+
946
+ # print('----------'*32)
947
+ # return qimage
948
+
949
+ def prepare_image_for_qimage(self, image_to_display, target_dims, luts, min=None, max=None, per_channel_normalization=False):
950
+ # here I will prepare the image to be dispalyed as a qimage --> it can handle normalization, etc...
951
+ # only keep from target_dims the hwc
952
+ # -> flatten before by taking 0
953
+
954
+ # we reduce the image until we have hwc left --> we stop at h unless we wanna do a max proj
955
+
956
+ if __DEBUG_NORMALIZATION__:
957
+ print('entering prepare_image_for_qimage')
958
+
959
+ # for normalization -−> global (whole stack), selected or projected, or custom defined by the user to some min and some max values
960
+ # --> TODO
961
+
962
+ # if image_to_display is None:
963
+ # return
964
+
965
+ list_of_true_indices = []
966
+ try:
967
+ list_of_true_indices = bool_list_to_index_list(self.channels)
968
+
969
+ # print('guess_dimensions(self.img)',self.channels, guess_dimensions(self.img),list_of_true_indices)
970
+
971
+ if not list_of_true_indices and 'c' in guess_dimensions(self.img):
972
+ list_of_true_indices = [i for i, _ in enumerate(self.channels)]
973
+
974
+ # print('list_of_true_indices', list_of_true_indices)
975
+ except Exception:
976
+ pass
977
+
978
+
979
+ # print('inside prepare_image_for_qimage')
980
+
981
+ per_channel = False
982
+ if self.normalization is not None:
983
+ if 'per_channel' in self.normalization:
984
+ per_channel = self.normalization['per_channel']
985
+
986
+ global_max = self.img.max()
987
+ global_min = self.img.min()
988
+
989
+
990
+ # else:
991
+
992
+ # default normalization -−> my favourite one definitely
993
+ min = global_min
994
+ max = global_max
995
+ # I can also do it locally if there are several channels but then I need to keep only the channels ones!!!
996
+
997
+ # print('shape of img for local', image_to_display.shape)
998
+
999
+
1000
+
1001
+ # if self.projection:
1002
+ # print('projection in prepare_image_for_qimage', self.projection)
1003
+
1004
+
1005
+ extra_dims =target_dims.replace('h','').replace('w','').replace('c','')
1006
+
1007
+ # print('extra_dims',extra_dims)
1008
+
1009
+ # the d version for normalization should be here
1010
+ # I haven't done the per channel but this could be made later too
1011
+
1012
+ final_target_dims = target_dims
1013
+ extra_dim_length = len(extra_dims)
1014
+
1015
+ if extra_dims:
1016
+ if __DEBUG_NORMALIZATION__:
1017
+ print('extra_dims inside',extra_dims, image_to_display.shape)
1018
+ for zzz,extra in enumerate(extra_dims):
1019
+ if __DEBUG_NORMALIZATION__:
1020
+ print('extra',extra)
1021
+
1022
+ # print('check',zzz, extra, extra_dim_length, zzz==extra_dim_length-1)
1023
+
1024
+ if self.projection and zzz==extra_dim_length-1:
1025
+ # Apply optional Z-range subset before projecting
1026
+ n_z = image_to_display.shape[0]
1027
+ z_s = int(self.projection_z_start) if self.projection_z_start is not None else 0
1028
+ z_e = int(self.projection_z_end) if self.projection_z_end is not None else n_z
1029
+ z_s = int(np.clip(z_s, 0, n_z - 1))
1030
+ z_e = int(np.clip(z_e, z_s + 1, n_z))
1031
+ if z_s != 0 or z_e != n_z:
1032
+ image_to_display = image_to_display[z_s:z_e]
1033
+ if self.projection == 'max':
1034
+ image_to_display = max_proj(image_to_display)
1035
+ elif self.projection == 'min':
1036
+ image_to_display = min_proj(image_to_display)
1037
+ else:
1038
+ image_to_display = avg_proj(image_to_display)
1039
+ else:
1040
+ image_to_display=image_to_display[0]
1041
+ final_target_dims=final_target_dims.replace(extra,'')
1042
+
1043
+ if self.normalization is not None:
1044
+ if __DEBUG_NORMALIZATION__:
1045
+ if self.normalization:
1046
+ print(self.normalization, self.normalization['range'], self.normalization['min_value'], self.normalization['max_value'])
1047
+ if 'range' in self.normalization and self.normalization['range'] == 'display':
1048
+ # only pay these two full passes when the 'display' range is requested
1049
+ min = image_to_display.min()
1050
+ max = image_to_display.max()
1051
+
1052
+ if 'range' in self.normalization and self.normalization['range'] == 'custom':
1053
+ min = self.normalization.get('min_value', global_min)
1054
+ max = self.normalization.get('max_value', global_max)
1055
+
1056
+
1057
+
1058
+ if True:
1059
+ # we perform the normalization here!!!
1060
+
1061
+ # print('image_to_display.max() before', image_to_display.max())
1062
+ # print('image_to_display.min() before', image_to_display.min())
1063
+
1064
+ if __DEBUG_NORMALIZATION__:
1065
+ print('used_min 1', min)
1066
+ # print(image_to_display.shape)
1067
+ print('used_max 1', max)
1068
+
1069
+ try:
1070
+
1071
+ if __DEBUG_NORMALIZATION__:
1072
+ print('main entrance', min, max)
1073
+
1074
+ # NB: normalization is now done in a single pass per channel through
1075
+ # _normalize_to_uint8 (LUT for integer images) — no float64 full copy
1076
+
1077
+ if not per_channel and not 'c' in target_dims: # global
1078
+ if __DEBUG_NORMALIZATION__:
1079
+ print('NEXT STEP', min, max, self.normalization) # was ok and then I fuck it
1080
+ if 'c' in guess_dimensions(self.img) and list_of_true_indices:
1081
+ # case of a single channel yet global max and min required but for a single channel
1082
+ # case there is a single channel selected --> yet using global max
1083
+ # dirty fix for display overwriting error
1084
+ if not self.normalization or self.normalization.get('range', 'all') == 'all':
1085
+ max = global_max
1086
+ min = global_min
1087
+ else:
1088
+ max = global_max
1089
+ min = global_min
1090
+
1091
+ if __DEBUG_NORMALIZATION__:
1092
+ print('NEXT STEP2', min, max)
1093
+
1094
+ if self.normalization is not None:
1095
+ # print('entering')
1096
+ if 'range' in self.normalization and self.normalization['range'] == 'custom':
1097
+ min = self.normalization.get('min_value', global_min)
1098
+ max = self.normalization.get('max_value', global_max)
1099
+ if 'range' in self.normalization and self.normalization['range'] == 'all':
1100
+ min = global_min
1101
+ max = global_max
1102
+ else:
1103
+ min = global_min
1104
+ max = global_max
1105
+
1106
+ if __DEBUG_NORMALIZATION__:
1107
+ print('NEXT STEP3', min, max)
1108
+
1109
+ if max == min:
1110
+ image_to_display = np.full(image_to_display.shape, _flat_level(max, self.img.dtype), dtype=np.uint8)
1111
+ else:
1112
+ image_to_display = _normalize_to_uint8(image_to_display, min, max)
1113
+ else:
1114
+ if __DEBUG_NORMALIZATION__:
1115
+ print('per_channel', min, max, list_of_true_indices, target_dims) # too many dims --> error
1116
+
1117
+ if not list_of_true_indices and 'c' in target_dims:
1118
+ list_of_true_indices = [i for i in range(self.img.shape[-1])]
1119
+
1120
+ if __DEBUG_NORMALIZATION__:
1121
+ print('vale before -1', min, max, list_of_true_indices)
1122
+ if list_of_true_indices:
1123
+ if 'c' in target_dims:
1124
+ normalized = np.empty(image_to_display.shape, dtype=np.uint8)
1125
+ for ccc,ch in enumerate(list_of_true_indices):
1126
+ if per_channel and self.normalization and self.normalization.get('range') == 'display' and 'c' in target_dims:
1127
+ max = image_to_display[...,ccc].max()
1128
+ min = image_to_display[...,ccc].min()
1129
+ if per_channel and ((self.normalization and self.normalization.get('range', 'all') == 'all') or not self.normalization):
1130
+ max = self.img[...,ch].max()
1131
+ min = self.img[...,ch].min()
1132
+
1133
+ if self.normalization is not None:
1134
+ if 'range' in self.normalization and self.normalization['range'] == 'custom':
1135
+ min = self.normalization.get('min_value', global_min)
1136
+ max = self.normalization.get('max_value', global_max)
1137
+
1138
+ if __DEBUG_NORMALIZATION__:
1139
+ print('selected max min ch', ccc, 'vs',ch, self.img.shape,image_to_display.shape, min, max, self,list_of_true_indices, target_dims)
1140
+ if max == min:
1141
+ if 'c' in target_dims:
1142
+ normalized[...,ccc] = _flat_level(max, self.img.dtype)
1143
+ else:
1144
+ image_to_display = np.full(image_to_display.shape, _flat_level(max, self.img.dtype), dtype=np.uint8)
1145
+ else:
1146
+ if 'c' in target_dims:
1147
+ normalized[...,ccc] = _normalize_to_uint8(image_to_display[...,ccc], min, max)
1148
+ else:
1149
+ image_to_display = _normalize_to_uint8(image_to_display, min, max)
1150
+ if 'c' in target_dims:
1151
+ image_to_display = normalized
1152
+ else:
1153
+ if __DEBUG_NORMALIZATION__:
1154
+ print('no channel found --> normalization done independently')
1155
+ image_to_display = _normalize_to_uint8(image_to_display, min, max)
1156
+
1157
+ if __DEBUG_NORMALIZATION__:
1158
+ print('image_to_display.max() after2', image_to_display.max())
1159
+ print('image_to_display.min() after2', image_to_display.min())
1160
+ except Exception:
1161
+ traceback.print_exc()
1162
+
1163
+ if __DEBUG_NORMALIZATION__:
1164
+ print('max-min exit', max - min)
1165
+
1166
+ # can do the same with the per_channel version −−> for which I should loop
1167
+
1168
+
1169
+ # print('final_target_dims', final_target_dims, 'target_dims',target_dims)
1170
+
1171
+ # in fact if any projection needs be done it should be done here
1172
+ # maybe I could also exclude some hot pixels in there --> see how I can do that
1173
+
1174
+ # target dim should be hw or hwc!!!
1175
+ # try to apply LUT maybe here
1176
+
1177
+ # maybe here --> if no L
1178
+
1179
+ if luts:
1180
+ if __DEBUG_NORMALIZATION__:
1181
+ print('ENTERING LUTS', min, max)
1182
+ # print('oubsi')
1183
+ available_luts = list_available_luts()
1184
+ for iii, lut in enumerate(luts):
1185
+ if not isinstance(lut, np.ndarray):
1186
+ # print('palette')
1187
+
1188
+ # else:
1189
+ # print(lut)
1190
+ lutcreator = PaletteCreator()
1191
+ lut_list = lutcreator.list
1192
+ # lut = lutcreator.create3(luts[lut])
1193
+
1194
+ # if isinstance(lut, list):
1195
+ # print('to be fixed',lut) # [255, 0, 0, 0] --> convert this to a lut --> easy
1196
+
1197
+ if isinstance(lut, str):
1198
+ if lut in available_luts:
1199
+ try:
1200
+ luts[iii] = np.asarray(lutcreator.create3(lut_list[lut]))
1201
+ except Exception:
1202
+ traceback.print_exc()
1203
+ luts[iii] = None
1204
+ elif isinstance(lut, list) and len(lut)==4:
1205
+ luts[iii]=lsm_LUT_to_numpy(lut)
1206
+ else:
1207
+ if __DEBUG_NORMALIZATION__:
1208
+ print('FAILEUR NO LUT CASE', min, max)
1209
+ pass
1210
+
1211
+
1212
+ merge = None
1213
+ if 'c' in target_dims:
1214
+ if __DEBUG_NORMALIZATION__:
1215
+ print('ENTERING LAST C LOOP', min, max)
1216
+ # print('oubsi2')
1217
+
1218
+ list_of_true_indices = []
1219
+ try:
1220
+ list_of_true_indices = bool_list_to_index_list(self.channels)
1221
+ # print('list_of_true_indices', list_of_true_indices)
1222
+ if not list_of_true_indices:
1223
+ list_of_true_indices = [i for i, _ in enumerate(self.channels)]
1224
+ except Exception:
1225
+ pass
1226
+ # apply LUT if there is a LUT
1227
+
1228
+ # why do I loop this 4 times whereas I just want one ???
1229
+ for ch in range(image_to_display.shape[-1]):
1230
+ if __DEBUG_NORMALIZATION__:
1231
+ print('channel', ch, image_to_display.shape)
1232
+
1233
+ img = image_to_display[...,ch]
1234
+
1235
+ # print('img slice shape', img.shape)
1236
+
1237
+
1238
+
1239
+
1240
+
1241
+ # local_min = img.min()
1242
+ # local_max = img.max()
1243
+
1244
+
1245
+
1246
+
1247
+ # we normalize the images always in the same way and as defined by the user
1248
+ # if max!=min:
1249
+ # img = ((img-min)/(max-min+1e-9))*255. # we just avoid div by 0
1250
+ # img = np.clip(img, 0, 255).astype(np.uint8)
1251
+ # I can also use the global if wanted
1252
+
1253
+ # I need to get the mode for the normalization
1254
+
1255
+
1256
+ # print('checks',luts, image_to_display.shape[-1]==len(luts), image_to_display.shape[-1],len(luts))
1257
+
1258
+
1259
+
1260
+ if luts is not None and image_to_display.shape[-1]==len(luts) and luts[ch] is not None:
1261
+
1262
+ if __DEBUG_NORMALIZATION__:
1263
+ print('entering LUT finalization')
1264
+ # print('oubsi3')
1265
+ # img = ((img-min)/(max-min))*255.
1266
+ # img = np.clip(img, 0,255) # force all stuff that need a lut clip to something like that
1267
+ # img = img.astypr(np.uint8)
1268
+ #
1269
+ # now I apply the LUT
1270
+
1271
+ # print('ch inside', ch, len(luts), luts[ch].shape)
1272
+
1273
+ # img = apply_lut(img, luts[ch], convert_to_RGB=True, min=min, max=max)
1274
+ img = apply_lut(img, luts[ch], convert_to_RGB=True, do_not_interpolate=True) # is it renormalizing here --> maybe I should prevent it
1275
+
1276
+ # img=luts[ch][img]
1277
+
1278
+
1279
+ # print('oubsi3-2', img.shape)
1280
+
1281
+ # plt.close('all')
1282
+ # plt.imshow(img)
1283
+ # plt.show()
1284
+
1285
+
1286
+
1287
+ else:
1288
+
1289
+ if __DEBUG_NORMALIZATION__:
1290
+ print('SKIPPING LUT')
1291
+ # print('dabouyo')
1292
+ # print('img.shape', img.shape)
1293
+ # print('*img.shape',*img.shape)
1294
+ # print('(*img.shape, 3)',(*img.shape, 3))
1295
+
1296
+ # I still need to normalize the image in there !!!
1297
+
1298
+ try:
1299
+ real_ch = list_of_true_indices[ch]
1300
+ except Exception:
1301
+ real_ch=ch
1302
+ tmp = np.zeros(shape=(*img.shape, 3), dtype=np.uint8)
1303
+
1304
+ if __DEBUG_NORMALIZATION__:
1305
+ print('dabouyo2')
1306
+ if real_ch < tmp.shape[-1]:
1307
+ tmp[..., real_ch] = img
1308
+ else:
1309
+ # print('too many channels to make any sense anyway --> setting to white')
1310
+ tmp[..., 0] = img
1311
+ tmp[..., 1] = img
1312
+ tmp[..., 2] = img
1313
+ # otherwise I would need to apply LUTs in a specified order -−> and maybe loop
1314
+ # or worst case scenario --> do it white
1315
+
1316
+ # print('dabouyo3')
1317
+ img = tmp # already uint8, no extra copy needed
1318
+ # print('dabouyo4')
1319
+
1320
+ # print('dabouyo end of shape', img.shape)
1321
+
1322
+
1323
+ # I also need to normalize the image like the others --> see how to do that
1324
+
1325
+
1326
+
1327
+
1328
+ # print('snip')
1329
+ if merge is None:
1330
+ # print('in there')
1331
+ merge = img
1332
+ # print('merge after init', merge.shape)
1333
+ else:
1334
+ # print('in there2')
1335
+
1336
+
1337
+ # if not merge.dtype == i
1338
+
1339
+ merge = np.maximum(merge, img)
1340
+ # print('snap')
1341
+ # print('merge end of loop', merge.shape)
1342
+
1343
+ # print(merge)
1344
+
1345
+
1346
+ # WHY THE FUCK IS THIS NEVER REACHED !!!!
1347
+ # print('snip2')
1348
+ # if merge is not None:
1349
+ # print('inside da stuff', merge.shape)
1350
+ # else:
1351
+ # print('not merged')
1352
+ # print('snap2')
1353
+
1354
+
1355
+
1356
+ self.qimage = toQimage(merge, normalize=False,dimensions=final_target_dims, luts=None)
1357
+ # self.qimage = toQimage(merge, dimensions=None, luts=None)
1358
+
1359
+ if __DEBUG_NORMALIZATION__:
1360
+ print('SUCCESS!!!')
1361
+ # else:
1362
+ #
1363
+ # self.qimage = toQimage(image_to_display, normalize=False, dimensions=final_target_dims, luts=None)
1364
+
1365
+ # if lut needs be applied and image is not normalized then need normalize it
1366
+ else:
1367
+ if __DEBUG_NORMALIZATION__:
1368
+ print('ENTERING LAST C LOOP ALTERNATIVE', min, max)
1369
+ # I still need to apply the lut if it exists
1370
+ # no channels -−> we can still apply the LUT
1371
+ if self.LUTs:
1372
+ if __DEBUG_NORMALIZATION__:
1373
+ print('ENTERING LAST C LOOP ALTERNATIVE part1', min, max, len(self.LUTs))
1374
+ # code duplication --> shall I really do that ??? --> maybe have a force gray that makes it gray instead -− >better
1375
+ luts=[]
1376
+ available_luts = list_available_luts()
1377
+ for iii, lut in enumerate(self.LUTs):
1378
+ if not isinstance(lut, np.ndarray):
1379
+ # print('palette')
1380
+ # else:
1381
+ # print(lut)
1382
+ lutcreator = PaletteCreator()
1383
+ lut_list = lutcreator.list
1384
+ # lut = lutcreator.create3(luts[lut])
1385
+ if lut in available_luts:
1386
+ try:
1387
+ luts.append(np.asarray(lutcreator.create3(lut_list[lut])))
1388
+ except Exception:
1389
+ traceback.print_exc()
1390
+ # luts[iii] = None
1391
+ luts.append(None)
1392
+ else:
1393
+ luts.append(None)
1394
+ else:
1395
+ luts.append(lut)
1396
+ if luts:
1397
+
1398
+ if __DEBUG_NORMALIZATION__:
1399
+ print('THERE IS A LUT',len(luts)) # there is a bug in the the right lut is not selected
1400
+ list_of_true_indices = []
1401
+ try:
1402
+ list_of_true_indices = bool_list_to_index_list(self.channels)
1403
+ # print('list_of_true_indices', list_of_true_indices)
1404
+ if not list_of_true_indices:
1405
+ list_of_true_indices = [i for i, _ in enumerate(self.channels)]
1406
+ except Exception:
1407
+ pass
1408
+
1409
+
1410
+ # image_to_display=luts[0][image_to_display]
1411
+ # just need the mapped lut else lut[0]
1412
+ # try:
1413
+ # image_to_display = apply_lut(image_to_display,luts[list_of_true_indices[0]], convert_to_RGB=True)
1414
+ # except:
1415
+ # I need really get the right index
1416
+
1417
+ # print('I am called with index 0', self.channels, list_of_true_indices)
1418
+
1419
+ # there is a bug there if the image has multichannels I need get the real index of the selected channel --> this will not be 0
1420
+ if list_of_true_indices: # seems to be required for proper mapping of LUTs for single channel images but maybe check
1421
+
1422
+
1423
+ # print('mode1')
1424
+ try:
1425
+ if __DEBUG_NORMALIZATION__:
1426
+ print('mode2', list_of_true_indices, len(luts)) # there are two luts --> see
1427
+ image_to_display = apply_lut(image_to_display, luts[list_of_true_indices[0]],
1428
+ convert_to_RGB=True, do_not_interpolate=True)
1429
+ except Exception:
1430
+ if __DEBUG_NORMALIZATION__:
1431
+ print('mode3')
1432
+ # try:
1433
+ # image_to_display = apply_lut(image_to_display, luts[0],
1434
+ # convert_to_RGB=True, do_not_interpolate=True)
1435
+ # except:
1436
+ # print('mode3b')
1437
+ # pass
1438
+ pass
1439
+ else:
1440
+ if __DEBUG_NORMALIZATION__:
1441
+ print('mode4')
1442
+ image_to_display = apply_lut(image_to_display, luts[0],
1443
+ convert_to_RGB=True, do_not_interpolate=True)
1444
+
1445
+ # img = lut[ch][img]
1446
+ # print('inside da stuff2', image_to_display.shape)
1447
+ # self.qimage = toQimage(image_to_display, dimensions=final_target_dims,luts=luts)
1448
+
1449
+ # print('passing there', image_to_display.shape,self.qimage , min, max)
1450
+ # so this means
1451
+
1452
+ self.qimage = toQimage(image_to_display, normalize=False,dimensions=final_target_dims,luts=None)
1453
+
1454
+ if __DEBUG_NORMALIZATION__:
1455
+ print('out final', image_to_display.shape, image_to_display.max(),image_to_display.min(), min, max, self.qimage) # min and max are incorrect too
1456
+ # else just return stuff !!!!
1457
+ del image_to_display
1458
+
1459
+ def remove_outliers(self, lower_cutoff=None, upper_cutoff=0.001, per_channel=False):
1460
+
1461
+ try:
1462
+ # print('#-'*32)
1463
+ self.restore_outliers()
1464
+
1465
+ if not 'c' in guess_dimensions(self.img):
1466
+ per_channel=False
1467
+
1468
+ if per_channel and isinstance(self.img, np.ndarray) and 'c' in guess_dimensions(self.img):
1469
+ self.lower_indices=[]
1470
+ self.upper_indices=[]
1471
+ self.lower_values=[]
1472
+ self.upper_values=[]
1473
+
1474
+ if not lower_cutoff and not upper_cutoff:
1475
+ return
1476
+
1477
+ if self.img.max() == self.img.min():
1478
+ return
1479
+
1480
+ for ch in range(self.img.shape[-1]):
1481
+ one_channel_img = self.img[...,ch]
1482
+
1483
+ if one_channel_img.max()==one_channel_img.min():
1484
+ self.lower_indices.append(None)
1485
+ self.upper_indices.append(None)
1486
+ self.lower_values.append(None)
1487
+ self.upper_values.append(None)
1488
+ continue
1489
+
1490
+ if upper_cutoff:
1491
+ # Find indices of values above upper_threshold
1492
+ max = np.percentile(one_channel_img, 100. * (1. - upper_cutoff))
1493
+ self.upper_indices.append(np.where(one_channel_img > max))
1494
+ # Store original values
1495
+ self.upper_values.append(one_channel_img[self.upper_indices[-1]])
1496
+ # Replace outliers with placeholders
1497
+ one_channel_img[self.upper_indices[-1]] = max
1498
+
1499
+ if lower_cutoff:
1500
+ # Find indices of values below lower_threshold
1501
+ min = np.percentile(img, 100. * lower_cutoff)
1502
+ self.lower_indices.append(np.where(one_channel_img < min))
1503
+ # Store original values
1504
+ self.lower_values.append(one_channel_img[self.lower_indices[-1]])
1505
+ # Replace outliers with placeholders
1506
+ one_channel_img[self.lower_indices[-1]] = min
1507
+
1508
+ # NB: one_channel_img is a view of self.img --> mutations above already
1509
+ # landed in self.img, no copy-back needed
1510
+ else:
1511
+ self.lower_indices = None
1512
+ self.upper_indices = None
1513
+ self.lower_values = None
1514
+ self.upper_values = None
1515
+
1516
+ if not lower_cutoff and not upper_cutoff:
1517
+ return
1518
+
1519
+ if self.img.max() == self.img.min():
1520
+ return
1521
+
1522
+ if upper_cutoff:
1523
+ # Find indices of values above upper_threshold
1524
+ max = np.percentile(self.img, 100. * (1. - upper_cutoff))
1525
+ self.upper_indices = np.where(self.img > max)
1526
+ # Store original values
1527
+ self.upper_values = self.img[self.upper_indices]
1528
+ # Replace outliers with placeholders
1529
+ self.img[self.upper_indices] = max
1530
+
1531
+ # print('gripsous', max)
1532
+
1533
+
1534
+ if lower_cutoff:
1535
+ # Find indices of values below lower_threshold
1536
+ min = np.percentile(img, 100. * lower_cutoff)
1537
+ self.lower_indices = np.where(self.img < min)
1538
+ # Store original values
1539
+ self.lower_values = self.img[self.lower_indices]
1540
+ # Replace outliers with placeholders
1541
+ self.img[self.lower_indices] = min
1542
+
1543
+ # print('-+' * 32)
1544
+ except Exception:
1545
+ pass
1546
+
1547
+
1548
+ # return self.lower_indices, self.lower_values, self.upper_indices, self.upper_values
1549
+
1550
+ def restore_outliers(self):
1551
+ try:
1552
+ # print('in restore_outliers ', self.img.max())
1553
+ # Restore original values
1554
+ if self.lower_indices is not None:
1555
+ if isinstance(self.lower_indices, list):
1556
+ for iii, indices in enumerate(self.lower_indices):
1557
+ if indices is not None:
1558
+ # self.img[indices,...,iii] = self.lower_values[iii]
1559
+ tmp = self.img[..., iii]
1560
+ tmp[indices] = self.lower_values[iii]
1561
+ self.img[..., iii] = tmp
1562
+ else:
1563
+ self.img[self.lower_indices] = self.lower_values
1564
+ if self.upper_indices is not None:
1565
+ # print('in restore_outliers 2')
1566
+
1567
+
1568
+ if isinstance(self.upper_indices, list):
1569
+ # print('in restore_outliers 3')
1570
+ for iii,indices in enumerate(self.upper_indices):
1571
+ if indices is not None:
1572
+ tmp = self.img[...,iii]
1573
+ tmp[indices] = self.upper_values[iii]
1574
+ self.img[..., iii]=tmp
1575
+ else:
1576
+ # print('in restore_outliers 4')
1577
+ # print('beforere',self.upper_values.max(), self.img.max())
1578
+
1579
+ self.img[self.upper_indices] = self.upper_values
1580
+ # print('afeter', self.upper_values.max(), self.img.max())
1581
+ else:
1582
+ # print('in restore_outliers NONE FOUND')
1583
+ pass
1584
+ # print('outside restore_outlier', self.img.max())
1585
+ except Exception:
1586
+ pass
1587
+
1588
+ def reset_outliers(self):
1589
+ try:
1590
+ # print('reset_outliers called')
1591
+ try:
1592
+ del self.lower_indices
1593
+ del self.lower_values
1594
+ except Exception:
1595
+ pass
1596
+
1597
+ self.lower_indices = None
1598
+ self.lower_values = None
1599
+
1600
+ try:
1601
+ del self.upper_indices
1602
+ del self.upper_values
1603
+ except Exception:
1604
+ pass
1605
+
1606
+ self.upper_indices = None
1607
+ self.upper_values = None
1608
+ except Exception:
1609
+ pass
1610
+
1611
+ # centralized code for
1612
+ def get_raw_size(self):
1613
+ if self.img is not None and isinstance(self.img, np.ndarray):
1614
+ # Primary: use metadata dimensions string (reliable for all our readers).
1615
+ try:
1616
+ w = self.img.get_width()
1617
+ h = self.img.get_height()
1618
+ if w is not None and h is not None:
1619
+ return w, h
1620
+ except Exception:
1621
+ pass
1622
+ # Fallback: shape heuristic for plain ndarrays without Img metadata.
1623
+ if self.img.ndim == 2:
1624
+ return self.img.shape[-1], self.img.shape[-2]
1625
+ elif self.img.ndim >= 3:
1626
+ # If last axis is small relative to neighbours → it's channels (HWC).
1627
+ if self.img.shape[-1] < self.img.shape[-2] or self.img.shape[-1] < self.img.shape[-3]:
1628
+ return self.img.shape[-2], self.img.shape[-3] # w, h (HWC)
1629
+ else:
1630
+ return self.img.shape[-1], self.img.shape[-2] # w, h (CHW or no-C)
1631
+ elif isinstance(self.img, plt.Figure):
1632
+ rect = get_fig_rect(self.img)
1633
+ w = rect.width()
1634
+ h = rect.height()
1635
+ # print('hw is defined here 3', h, w, self.filename)
1636
+ return w,h
1637
+ elif isinstance(self.renderer, QSvgRenderer):
1638
+ # if figure is a svg file then get it from here
1639
+ w,h = self.renderer.defaultSize().width(),self.renderer.defaultSize().height()
1640
+ # print('hw is defined here 4', h, w, self.filename)
1641
+ return w,h
1642
+
1643
+ if isinstance(self.img, QRectF):
1644
+ return int(self.img.width()), int(self.img.height())
1645
+ # this is problematic and should never be reached!!!
1646
+
1647
+ # print('data to check',self.img, type(self.img), isinstance(self.img, QRectF), isinstance(self.img, str))
1648
+
1649
+ # raise Exception('do nbot reach that to avoid issues')
1650
+
1651
+ # if all failed just take the rect default size
1652
+ w = super().width()
1653
+ h = super().height()
1654
+ # print('hw is defined here 5', h, w, self.filename)
1655
+ return w,h
1656
+
1657
+
1658
+ def get_consolidated_filename(self, parent_file):
1659
+ self.filename = get_consolidated_filename_from_parent(self.filename, parent_file)
1660
+
1661
+
1662
+ def consolidate_names(self, files_to_consolidate, consolidated_files):
1663
+ if not self.filename:
1664
+ return
1665
+ if not is_string_a_list_string(self.filename):
1666
+ idx = files_to_consolidate.index(self.filename)
1667
+ if idx!=-1:
1668
+ # if self.filename in files_to_consolidate:
1669
+ self.filename = consolidated_files[idx]
1670
+ else:
1671
+ files = reload_string_list(self.filename)
1672
+ consolidated_file_names = []
1673
+ for file in files:
1674
+ idx = files_to_consolidate.index(file)
1675
+ if idx != -1:
1676
+ # if self.filename in files_to_consolidate:
1677
+ consolidated_file_names.append(consolidated_files[idx])
1678
+ self.filename = str(consolidated_file_names)
1679
+
1680
+
1681
+
1682
+ def _toBuffer(self, bufferType='raster', apply_publication=False):
1683
+ if self.img is not None:
1684
+ buf = io.BytesIO()
1685
+ saved = self._apply_publication_style() if apply_publication else None
1686
+ try:
1687
+ import matplotlib as mpl
1688
+ rc_overrides = self._publication_rc_overrides() if apply_publication else {}
1689
+ # settings the plot was made with (e.g. LaTeX text: text.usetex and its
1690
+ # preamble are read when the plot is drawn, not when it is made)
1691
+ rc_overrides = {**(getattr(self.img, '_pyf_rc', None) or {}), **rc_overrides}
1692
+ if bufferType != 'raster' and Image2D._export_editable_text:
1693
+ rc_overrides['svg.fonttype'] = 'none'
1694
+ from batoolset.matplolibs.tools import figure_pad_inches
1695
+ pad = figure_pad_inches(self.img) # same pad get_fig_rect sized the panel with
1696
+ with mpl.rc_context(rc_overrides):
1697
+ if bufferType == 'raster':
1698
+ self.img.savefig(buf, format='png', bbox_inches='tight', pad_inches=pad)
1699
+ else:
1700
+ self.img.savefig(buf, format='svg', bbox_inches='tight', pad_inches=pad)
1701
+ finally:
1702
+ if saved is not None:
1703
+ self._restore_publication_style(saved)
1704
+ buf.seek(0)
1705
+ return buf
1706
+ return None
1707
+
1708
+ def _publication_family(self):
1709
+ """The figure font for the plot's text; None for a plot typeset by LaTeX
1710
+ (usetex knows no system font names: it would fall back to serif)."""
1711
+ if (getattr(self.img, '_pyf_rc', None) or {}).get('text.usetex'):
1712
+ return None
1713
+ return getattr(self, 'publication_font', None)
1714
+
1715
+ def _publication_rc_overrides(self):
1716
+ font_size = getattr(self, 'publication_font_size', 12)
1717
+ line_width = getattr(self, 'publication_line_width', 0.65)
1718
+ font_family = self._publication_family()
1719
+ overrides = {
1720
+ 'font.size': font_size,
1721
+ 'axes.titlesize': font_size,
1722
+ 'axes.labelsize': font_size,
1723
+ 'xtick.labelsize': font_size,
1724
+ 'ytick.labelsize': font_size,
1725
+ 'legend.fontsize': font_size,
1726
+ 'lines.linewidth': line_width,
1727
+ 'axes.linewidth': line_width,
1728
+ 'xtick.major.width': line_width,
1729
+ 'ytick.major.width': line_width,
1730
+ 'xtick.minor.width': line_width,
1731
+ 'ytick.minor.width': line_width,
1732
+ 'patch.linewidth': line_width,
1733
+ }
1734
+ if font_family:
1735
+ overrides['font.family'] = font_family
1736
+ return overrides
1737
+
1738
+ def _apply_publication_style(self):
1739
+ import matplotlib.text as mtext
1740
+ font_size = getattr(self, 'publication_font_size', 12)
1741
+ line_width = getattr(self, 'publication_line_width', 0.65)
1742
+ font_family = self._publication_family()
1743
+ saved_lw = []
1744
+ saved_fs = []
1745
+ for artist in self.img.findobj(lambda a: hasattr(a, 'set_linewidth') and hasattr(a, 'get_linewidth')):
1746
+ try:
1747
+ lw = artist.get_linewidth()
1748
+ artist.set_linewidth(line_width)
1749
+ saved_lw.append((artist, lw))
1750
+ except Exception:
1751
+ pass
1752
+ for text in self.img.findobj(mtext.Text):
1753
+ try:
1754
+ fs = text.get_fontsize()
1755
+ ff = text.get_fontfamily()
1756
+ text.set_fontsize(font_size)
1757
+ if font_family is not None:
1758
+ text.set_fontfamily(font_family)
1759
+ saved_fs.append((text, fs, ff))
1760
+ except Exception:
1761
+ pass
1762
+ return {'lw': saved_lw, 'fs': saved_fs}
1763
+
1764
+ def _restore_publication_style(self, saved):
1765
+ for artist, lw in saved['lw']:
1766
+ try:
1767
+ artist.set_linewidth(lw)
1768
+ except Exception:
1769
+ pass
1770
+ for text, fs, ff in saved['fs']:
1771
+ try:
1772
+ text.set_fontsize(fs)
1773
+ text.set_fontfamily(ff)
1774
+ except Exception:
1775
+ pass
1776
+
1777
+ def _transform_svg_bytes(self, svg_bytes):
1778
+ """Return svg_bytes with stroke widths set to an absolute value and font family replaced."""
1779
+ import re
1780
+ stroke_width = getattr(self, 'publication_svg_stroke_width', None)
1781
+ font_family = self._publication_family()
1782
+ if stroke_width is None and font_family is None:
1783
+ return svg_bytes
1784
+ text = svg_bytes.decode('utf-8') if isinstance(svg_bytes, bytes) else svg_bytes
1785
+
1786
+ if stroke_width is not None:
1787
+ replacement = f'{stroke_width:.4g}pt'
1788
+ text = re.sub(r'(stroke-width\s*:\s*)[\d.]+(?:px|pt|em|%)?',
1789
+ lambda m: m.group(1) + replacement, text)
1790
+ text = re.sub(r'(stroke-width\s*=\s*")[\d.]+(?:px|pt|em|%)?(")',
1791
+ lambda m: m.group(1) + replacement + m.group(2), text)
1792
+ if font_family is not None:
1793
+ text = re.sub(r'(font-family\s*:\s*)([^;}"\']+)', lambda m: m.group(1) + font_family, text)
1794
+ text = re.sub(r'(font-family\s*=\s*")([^"]+)(")', lambda m: m.group(1) + font_family + m.group(3), text)
1795
+ return text.encode('utf-8')
1796
+
1797
+ def enter_export_mode(self):
1798
+ """Temporarily rebuild the renderer with publication style for export."""
1799
+ self._export_original_renderer = None
1800
+ editable_plot = Image2D._export_editable_text and isinstance(getattr(self, 'img', None), plt.Figure)
1801
+ if not getattr(self, 'publication_style', False) and not editable_plot:
1802
+ return
1803
+ if isinstance(getattr(self, 'img', None), plt.Figure):
1804
+ buf = self._toBuffer(bufferType='svg', apply_publication=bool(getattr(self, 'publication_style', False)))
1805
+ if buf is not None:
1806
+ self._export_original_renderer = self.renderer
1807
+ data = buf.read()
1808
+ buf.close()
1809
+ renderer = None
1810
+ if editable_plot:
1811
+ # the plot's text drawn as text (editable PowerPoint / Illustrator)
1812
+ from batoolset.drawings.svg_text_renderer import make_text_renderer
1813
+ renderer = make_text_renderer(data, make_renderer=_make_svg_renderer)
1814
+ if renderer is not None:
1815
+ renderer.setAspectRatioMode(Qt.AspectRatioMode.KeepAspectRatio)
1816
+ self.renderer = renderer if renderer is not None else _make_svg_renderer(data)
1817
+ elif isinstance(getattr(self, 'img', None), QSvgRenderer):
1818
+ raw = getattr(self, '_raw_svg_bytes', None)
1819
+ # a PDF (a finished plot) keeps its own line widths; its text is outlines anyway
1820
+ if isinstance(self.filename, str) and self.filename.lower().endswith('.pdf'):
1821
+ raw = None
1822
+ if raw is not None:
1823
+ transformed = self._transform_svg_bytes(raw)
1824
+ if transformed is not raw:
1825
+ from qtpy.QtCore import QByteArray
1826
+ self._export_original_renderer = self.renderer
1827
+ self.renderer = _make_svg_renderer(QByteArray(transformed))
1828
+
1829
+ def exit_export_mode(self):
1830
+ """Restore the original renderer after export."""
1831
+ if getattr(self, '_export_original_renderer', None) is not None:
1832
+ self.renderer = self._export_original_renderer
1833
+ self._export_original_renderer = None
1834
+
1835
+ def get_extra_border_size(self):
1836
+ if self.border_size is not None and self.border_color is not None:
1837
+ return self.border_size
1838
+ return 0
1839
+
1840
+ def is_template(self):
1841
+ return self.filename == '@template@'
1842
+
1843
+ def set_flip(self, flip_h=None, flip_v=None):
1844
+ """Mirror the displayed image left-right (flip_h) and/or top-bottom (flip_v); None keeps that axis."""
1845
+ if flip_h is not None:
1846
+ self.flip_h = bool(flip_h)
1847
+ if flip_v is not None:
1848
+ self.flip_v = bool(flip_v)
1849
+ self.update_qimage()
1850
+
1851
+ def set_rotation(self, theta): # by default set it to black or to None --> white could also be useful
1852
+ super().set_rotation(theta)
1853
+ self.theta = theta # CHECK WHY AND IF I NEED THE LINE ABOVE!!!
1854
+
1855
+ # ------------------------------------------------------------------
1856
+ # Display cache (interactive painting only — export always bypasses it
1857
+ # and renders full-resolution rasters / true vectors).
1858
+ # ------------------------------------------------------------------
1859
+
1860
+ def _display_cache_scale(self, painter):
1861
+ """Device pixels per logical unit under the current painter transform (incl. HiDPI)."""
1862
+ t = painter.worldTransform()
1863
+ sx = (t.m11() ** 2 + t.m12() ** 2) ** 0.5
1864
+ sy = (t.m21() ** 2 + t.m22() ** 2) ** 0.5
1865
+ scale = max(sx, sy)
1866
+ try:
1867
+ dpr = painter.device().devicePixelRatioF()
1868
+ except Exception:
1869
+ dpr = 1.0
1870
+ return max(scale * dpr, 0.01)
1871
+
1872
+ def _display_cache_get(self, key):
1873
+ cache = getattr(self, '_display_cache', None)
1874
+ if cache is not None and cache[0] == key:
1875
+ return cache
1876
+ return None
1877
+
1878
+ def _display_cache_put(self, key, pixmap, logical_w=0, logical_h=0):
1879
+ self._display_cache = (key, pixmap, logical_w, logical_h)
1880
+
1881
+ def invalidate_display_cache(self):
1882
+ self._display_cache = None
1883
+
1884
+ # very large images keep a <=4096px downsampled copy for interactive display;
1885
+ # export always reads the full-resolution qimage
1886
+ _DISPLAY_PROXY_MAX_DIM = 4096
1887
+
1888
+ def _update_display_proxy(self):
1889
+ """Invalidate the display proxy after self.qimage changed. The proxy itself is
1890
+ built lazily on first display use so figure loading never pays for it."""
1891
+ self._qimage_proxy = None
1892
+ self._proxy_stale = True
1893
+ self.invalidate_display_cache()
1894
+
1895
+ def _get_display_proxy(self):
1896
+ """Return the downsampled display proxy (built on demand), or None for
1897
+ images small enough to display directly."""
1898
+ if getattr(self, '_proxy_stale', True):
1899
+ self._proxy_stale = False
1900
+ self._qimage_proxy = None
1901
+ q = self.qimage
1902
+ if q is not None and not q.isNull():
1903
+ max_dim = max(q.width(), q.height())
1904
+ if max_dim > self._DISPLAY_PROXY_MAX_DIM:
1905
+ f = self._DISPLAY_PROXY_MAX_DIM / max_dim
1906
+ self._qimage_proxy = q.scaled(max(1, int(round(q.width() * f))),
1907
+ max(1, int(round(q.height() * f))),
1908
+ Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
1909
+ return getattr(self, '_qimage_proxy', None)
1910
+
1911
+ def _get_cached_raster_pixmap(self, painter, rect_to_plot, qsource):
1912
+ """Return a QPixmap of self.qimage cropped to qsource and pre-scaled to the
1913
+ on-screen size of rect_to_plot. Regenerated only when the source image, crop
1914
+ or display size changes."""
1915
+ scale = self._display_cache_scale(painter)
1916
+ pw = max(1, int(round(rect_to_plot.width() * scale)))
1917
+ ph = max(1, int(round(rect_to_plot.height() * scale)))
1918
+ src = qsource.toRect()
1919
+ # never store more pixels than the source has — the painter stretches the rest
1920
+ if pw >= src.width() or ph >= src.height():
1921
+ pw, ph = src.width(), src.height()
1922
+ key = ('img', self.qimage.cacheKey(), pw, ph,
1923
+ src.x(), src.y(), src.width(), src.height())
1924
+ cached = self._display_cache_get(key)
1925
+ if cached is not None:
1926
+ return cached[1]
1927
+ # source from the downsampled proxy when its resolution suffices (huge images);
1928
+ # at high zoom (pw/ph beyond proxy resolution) fall back to the full-res qimage
1929
+ source_img = self.qimage
1930
+ proxy = self._get_display_proxy()
1931
+ if proxy is not None and not proxy.isNull():
1932
+ f = proxy.width() / self.qimage.width()
1933
+ proxy_src = QRect(int(src.x() * f), int(src.y() * f),
1934
+ max(1, int(src.width() * f)), max(1, int(src.height() * f)))
1935
+ if pw <= proxy_src.width() and ph <= proxy_src.height():
1936
+ source_img = proxy
1937
+ src = proxy_src
1938
+ sub = source_img if src == source_img.rect() else source_img.copy(src)
1939
+ if (pw, ph) != (src.width(), src.height()):
1940
+ sub = sub.scaled(pw, ph, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
1941
+ pixmap = QPixmap.fromImage(sub)
1942
+ self._display_cache_put(key, pixmap)
1943
+ return pixmap
1944
+
1945
+ def _get_cached_rotated_pixmap(self, painter):
1946
+ """Return (pixmap, logical_w, logical_h) for the rotated+cropped image.
1947
+ The expensive rotate_image/crop_image pass only runs on cache miss.
1948
+ logical_w/h are full-resolution-equivalent dims (the pixmap itself may be
1949
+ the downsampled proxy for huge images), used to size the target rect."""
1950
+ key = ('rot', self.qimage.cacheKey(), self.theta,
1951
+ self.crop_left, self.crop_right, self.crop_top, self.crop_bottom)
1952
+ cached = self._display_cache_get(key)
1953
+ if cached is not None:
1954
+ return cached[1], cached[2], cached[3]
1955
+ source_img = self.qimage
1956
+ f = 1.0
1957
+ proxy = self._get_display_proxy()
1958
+ if proxy is not None and not proxy.isNull():
1959
+ source_img = proxy
1960
+ f = proxy.width() / self.qimage.width()
1961
+ rotated_image = rotate_image(source_img, self.theta, annotations=None, parent_painter=painter)
1962
+ cropped = crop_image(rotated_image,
1963
+ int(round((self.crop_left or 0) * f)), int(round((self.crop_right or 0) * f)),
1964
+ int(round((self.crop_top or 0) * f)), int(round((self.crop_bottom or 0) * f)))
1965
+ pixmap = QPixmap.fromImage(cropped)
1966
+ logical_w = cropped.width() / f
1967
+ logical_h = cropped.height() / f
1968
+ self._display_cache_put(key, pixmap, logical_w, logical_h)
1969
+ return pixmap, logical_w, logical_h
1970
+
1971
+ def _get_cached_vector_pixmap(self, painter, rect_to_plot):
1972
+ """Rasterize the SVG/matplotlib renderer once at the current on-screen size
1973
+ and reuse the pixmap on subsequent paints."""
1974
+ scale = self._display_cache_scale(painter)
1975
+ pw = max(1, min(int(round(rect_to_plot.width() * scale)), 8192))
1976
+ ph = max(1, min(int(round(rect_to_plot.height() * scale)), 8192))
1977
+ key = ('svg', self.renderer, pw, ph, self.theta,
1978
+ self.crop_left, self.crop_right, self.crop_top, self.crop_bottom)
1979
+ cached = self._display_cache_get(key)
1980
+ if cached is not None:
1981
+ return cached[1]
1982
+ pixmap = QPixmap(pw, ph)
1983
+ pixmap.fill(Qt.transparent)
1984
+ cache_painter = QPainter(pixmap)
1985
+ cache_painter.setRenderHint(QPainter.Antialiasing)
1986
+ cache_painter.setRenderHint(QPainter.TextAntialiasing)
1987
+ cache_painter.setRenderHint(QPainter.SmoothPixmapTransform)
1988
+ # map rect_to_plot logical coords onto the pixmap
1989
+ cache_painter.scale(pw / rect_to_plot.width(), ph / rect_to_plot.height())
1990
+ cache_painter.translate(-rect_to_plot.x(), -rect_to_plot.y())
1991
+ try:
1992
+ self._render_vector_content(cache_painter, rect_to_plot)
1993
+ finally:
1994
+ cache_painter.end()
1995
+ self._display_cache_put(key, pixmap)
1996
+ return pixmap
1997
+
1998
+ def _render_vector_content(self, painter, rect_to_plot):
1999
+ """Render the SVG/matplotlib renderer content into rect_to_plot, honouring
2000
+ crop and rotation. This is the true vector path used for export; interactive
2001
+ painting goes through _get_cached_vector_pixmap instead.
2002
+
2003
+ In an SVG export that embeds vector panels (svg_embed.collecting), the
2004
+ panel's own SVG source is placed instead of being drawn through Qt, so
2005
+ its text and clip paths are kept."""
2006
+ from batoolset.drawings import svg_embed
2007
+ source = getattr(self.renderer, '_pyf_svg_source', None)
2008
+ embed = source is not None and svg_embed.wants(painter)
2009
+ try:
2010
+ keep_aspect = self.renderer.aspectRatioMode() == Qt.AspectRatioMode.KeepAspectRatio
2011
+ except Exception:
2012
+ keep_aspect = True
2013
+ if self.crop_left or self.crop_right or self.crop_top or self.crop_bottom or self.theta:
2014
+ # crop and rotations for svg in the same way as for figs
2015
+ outer = painter.worldTransform()
2016
+ painter.save()
2017
+ w, h = self.get_raw_size()
2018
+ svg_rect = QRectF(0, 0, w, h)
2019
+ center = svg_rect.center()
2020
+
2021
+ rotation_transform = QTransform().translate(center.x(), center.y()).rotate(self.theta if self.theta else 0).translate(-center.x(), -center.y())
2022
+
2023
+ # Calculate the bounding rectangle of the rotated SVG
2024
+ bounding_rect = rotation_transform.mapRect(svg_rect)
2025
+
2026
+ # Crop the bounding rectangle
2027
+ cropped_bounding_rect = bounding_rect.adjusted(self.crop_left, self.crop_top, -self.crop_right, -self.crop_bottom)
2028
+
2029
+ self.clip_rect = rect_to_plot
2030
+
2031
+ # Calculate the scale factor to fit cropped_bounding_rect into self.clip_rect
2032
+ scale_factor = min(self.clip_rect.width() / cropped_bounding_rect.width(),
2033
+ self.clip_rect.height() / cropped_bounding_rect.height())
2034
+
2035
+ # Scale and translate the painter to align cropped_bounding_rect with self.clip_rect
2036
+ scaled_cropped_bounding_rect = QRectF(cropped_bounding_rect.x() * scale_factor, cropped_bounding_rect.y() * scale_factor, cropped_bounding_rect.width() * scale_factor, cropped_bounding_rect.height() * scale_factor)
2037
+ painter.translate(self.clip_rect.center() - scaled_cropped_bounding_rect.center())
2038
+ painter.scale(scale_factor, scale_factor)
2039
+
2040
+ if self.theta:
2041
+ painter.translate(svg_rect.center())
2042
+ painter.rotate(self.theta)
2043
+ painter.translate(-svg_rect.center())
2044
+
2045
+ if embed:
2046
+ # the visible panel (rect_to_plot, outer coordinates) in the
2047
+ # SVG's own coordinates: the clip of the embedded source
2048
+ inv, ok = painter.worldTransform().inverted()
2049
+ clip = (outer * inv).map(QPolygonF(QRectF(rect_to_plot))) if ok else None
2050
+ svg_embed.active().place(painter, svg_rect, source, clip=clip, keep_aspect=keep_aspect)
2051
+ else:
2052
+ self.renderer.render(painter, svg_rect)
2053
+ painter.restore()
2054
+ else:
2055
+ painter.save()
2056
+ if embed:
2057
+ svg_embed.active().place(painter, rect_to_plot, source, clip=QRectF(rect_to_plot),
2058
+ keep_aspect=keep_aspect)
2059
+ else:
2060
+ self.renderer.render(painter, rect_to_plot)
2061
+ painter.restore()
2062
+
2063
+ def draw_bg(self, painter, draw=True, parent=None, restore_painter_in_the_end=True):
2064
+
2065
+ # drawing was getting too big so I split the draw bg and draw annotations into two separate things
2066
+ if draw:
2067
+ painter.save()
2068
+ painter.setPen(Qt.NoPen)
2069
+ painter.setBrush(Qt.NoBrush)
2070
+
2071
+ if parent is None:
2072
+ # intersect, never replace: a figure replayed by the poster
2073
+ # designer (QPicture) must stay inside the poster's crop clip
2074
+ painter.setClipRect(self.getRect(all=True), Qt.ClipOperation.IntersectClip) # required first to fraw the stuff at the very beginning of it !!! --> it must be done before save
2075
+
2076
+ if self.fill_color is not None:
2077
+ painter.setBrush(QBrush(QColor(self.fill_color)))
2078
+ painter.drawRect(self.getRect(all=True))
2079
+
2080
+ try: # trick to force equal save and restore even upon error !!!
2081
+ painter.setOpacity(self.opacity)
2082
+
2083
+ if parent is not None:
2084
+ rect_to_plot = self._update_inset_scale(parent)
2085
+ rect_to_plot = rect_to_plot.translated(self.topLeft()) # very hacky way to get it to work --> see how I can do that # DEV NOT THIS IS MANDATORY TO GET THE INSETS TO DISPLAY --> NEVER REMOVE THIS
2086
+ else:
2087
+ rect_to_plot = self.getRect(all=True) # in that case I can add it a position
2088
+
2089
+ if self.img is not None and not isinstance(self.img, QRectF) and not isinstance(self.img, plt.Figure) and not isinstance(self.img, QSvgRenderer):
2090
+ x = 0
2091
+ y = 0
2092
+ # try:
2093
+ # w = self.img.get_width()
2094
+ # h = self.img.get_height()
2095
+ # except:
2096
+ # # see how to handle images that are not in the stuff
2097
+ # # TODO maybe fix that too
2098
+ # w = self.img.shape[1]
2099
+ # h = self.img.shape[0]
2100
+ #
2101
+ # if w is None or h is None:
2102
+ # w = self.img.shape[1]
2103
+ # h = self.img.shape[0]
2104
+ w,h = self.get_raw_size()
2105
+
2106
+ if not self.theta:
2107
+ if self.crop_top is not None:
2108
+ y = self.crop_top
2109
+ h -= self.crop_top
2110
+ if self.crop_left is not None:
2111
+ x = self.crop_left
2112
+ w -= self.crop_left
2113
+ if self.crop_right is not None:
2114
+ w -= self.crop_right
2115
+ if self.crop_bottom is not None:
2116
+ h -= self.crop_bottom
2117
+
2118
+ # pb here --> see how to really crop
2119
+ qsource = QRectF(x, y, w, h)
2120
+
2121
+ # TODO --> maybe have a look here some day https://stackoverflow.com/questions/15166754/stroking-a-path-only-inside-outside
2122
+
2123
+ if parent is not None and self.border_color is not None and self.border_size is not None and self.border_size >= 1: # we draw the square first then the image so that the image is not truncated by the bounding rect
2124
+ pen = QPen(QColor(self.border_color),
2125
+ self.border_size * 2.) # Adjust the color and line style as needed
2126
+ painter.setPen(pen)
2127
+ painter.drawRect(rect_to_plot)
2128
+
2129
+ if not self.theta:
2130
+ if check_antialiasing(painter):
2131
+ # export/production: draw from the full-resolution image
2132
+ painter.drawImage(rect_to_plot, self.qimage, qsource) # , flags=QtCore.Qt.AutoColor
2133
+ else:
2134
+ # interactive: blit a cached pixmap pre-scaled to screen size
2135
+ try:
2136
+ pixmap = self._get_cached_raster_pixmap(painter, rect_to_plot, qsource)
2137
+ painter.drawPixmap(rect_to_plot, pixmap, QRectF(pixmap.rect()))
2138
+ except Exception:
2139
+ traceback.print_exc()
2140
+ painter.drawImage(rect_to_plot, self.qimage, qsource)
2141
+ else:
2142
+ # VERY DIRTY HACK --> DRAW ALL THE NON PLACED SHAPES ON THE ORIGINAL IMAGE THEN ROTATE IT -−> MEGA DIRTY HACK BUT WILL WORK !!!
2143
+ x = 0
2144
+ y = 0
2145
+ # try:
2146
+ # w = self.img.get_width()
2147
+ # h = self.img.get_height()
2148
+ # except:
2149
+ # # see how to handle images that are not in the stuff
2150
+ # w = self.img.shape[1]
2151
+ # h = self.img.shape[0]
2152
+ #
2153
+ # if w is None or h is None:
2154
+ # w = self.img.shape[1]
2155
+ # h = self.img.shape[0]
2156
+
2157
+ # the rotate+crop pass is expensive --> cached for interactive painting
2158
+ # (recomputed only when the image, angle or crop changes); export
2159
+ # always recomputes from the full-resolution image
2160
+ # DEV NOT KEEP there is a bug BE CAREFUL CALLING A QRECTF(ANOTHER) WITH ANOTHER that has negative x sets it to 0 and reduces its width and height --> NO CLUE WHY !!!!
2161
+ rotated_pixmap = None
2162
+ if not check_antialiasing(painter):
2163
+ try:
2164
+ rotated_pixmap, source_w, source_h = self._get_cached_rotated_pixmap(painter)
2165
+ except Exception:
2166
+ traceback.print_exc()
2167
+ if rotated_pixmap is None:
2168
+ rotated_image = rotate_image(self.qimage, self.theta, annotations=None, parent_painter=painter)
2169
+ cropped_rotated_image = crop_image(rotated_image, self.crop_left, self.crop_right, self.crop_top, self.crop_bottom)
2170
+ source_w, source_h = cropped_rotated_image.width(), cropped_rotated_image.height()
2171
+
2172
+ rotated_image_rect = QRectF(0, 0, source_w, source_h)
2173
+ rotated_image_target = QRectF(rotated_image_rect.x(), rotated_image_rect.y(), rotated_image_rect.width(), rotated_image_rect.height())
2174
+ rotated_image_target.setWidth(rotated_image_target.width()/self.scale)
2175
+ rotated_image_target.setHeight(rotated_image_target.height()/self.scale)
2176
+
2177
+ rotated_image_target.translate(rect_to_plot.center()-rotated_image_target.center())
2178
+ if rotated_pixmap is not None:
2179
+ # blit only the part of the target visible in the viewport
2180
+ # (cheaper than scaling the whole possibly huge target); the
2181
+ # pixmap may be the downsampled proxy so the source rect is
2182
+ # mapped proportionally
2183
+ visible = rotated_image_target
2184
+ try:
2185
+ inv, ok = painter.worldTransform().inverted()
2186
+ if ok:
2187
+ dev_rect = inv.mapRect(QRectF(painter.viewport()))
2188
+ visible = rotated_image_target.intersected(dev_rect)
2189
+ except Exception:
2190
+ pass
2191
+ if not visible.isEmpty():
2192
+ fx = rotated_pixmap.width() / rotated_image_target.width()
2193
+ fy = rotated_pixmap.height() / rotated_image_target.height()
2194
+ src = QRectF((visible.x() - rotated_image_target.x()) * fx,
2195
+ (visible.y() - rotated_image_target.y()) * fy,
2196
+ visible.width() * fx, visible.height() * fy)
2197
+ painter.drawPixmap(visible, rotated_pixmap, src)
2198
+ else:
2199
+ painter.drawImage(rotated_image_target, cropped_rotated_image, rotated_image_rect)
2200
+
2201
+ elif self.renderer is not None or isinstance(self.img, plt.Figure):
2202
+ if self.crop_left or self.crop_right or self.crop_top or self.crop_bottom or self.theta:
2203
+ vector_target = self.getRect(all=True)
2204
+ else:
2205
+ vector_target = rect_to_plot
2206
+ if check_antialiasing(painter):
2207
+ # export/production: true vector rendering, publication quality
2208
+ self._render_vector_content(painter, vector_target)
2209
+ else:
2210
+ # interactive: blit a cached rasterization of the vector content
2211
+ try:
2212
+ pixmap = self._get_cached_vector_pixmap(painter, vector_target)
2213
+ painter.drawPixmap(vector_target, pixmap, QRectF(pixmap.rect()))
2214
+ except Exception:
2215
+ traceback.print_exc()
2216
+ self._render_vector_content(painter, vector_target)
2217
+
2218
+ else:
2219
+ if not check_antialiasing(painter): # only draw this in figure building mode, not in production
2220
+ if self.filename is None: # if the file has a name and no image was produced then there is an error somewhere and the user needs to enter custom code to handle things
2221
+ if self.isText:
2222
+ text = 'Label'
2223
+ pen = QPen(QColor(200, 255, 64)) # yellow
2224
+ else:
2225
+ if ((self.img is None or isinstance(self.img, QRectF)) and self.custom_loading_script):
2226
+ text = "Error"
2227
+ pen = QPen(QColor(255, 0, 0)) # red
2228
+ else:
2229
+ text = "Empty"
2230
+ pen = QPen(QColor(255, 0, 255)) # pu
2231
+ else:
2232
+ if self.filename == '@template@':
2233
+ text = "Template"
2234
+ pen = QPen(QColor(255, 165, 0)) # orange
2235
+ else:
2236
+ text = "Error"
2237
+ pen = QPen(QColor(255, 0, 0)) # red
2238
+ # TODO deactivate this for production but keep it for image editing!
2239
+
2240
+ pen.setWidthF(6)
2241
+ painter.setPen(pen)
2242
+ painter.drawRect(rect_to_plot) # this fills the rect I guess I rather wanna have a pen rather than a brush and a --> probably not what I want !!!
2243
+
2244
+ font = painter.font() # Use the current font or set a custom font if desired
2245
+
2246
+ # Calculate the text's center position within the rectangle
2247
+ font_metrics = QFontMetrics(font)
2248
+ text_width = font_metrics.width(text)
2249
+ text_height = font_metrics.height()
2250
+ text_x = rect_to_plot.x()+ rect_to_plot.width() / 2. - text_width / 2.
2251
+ text_y = rect_to_plot.y()+text_height# + rect_to_plot.height() / 2. + text_height / 2.+text_height # to make it more visible
2252
+
2253
+ # Draw the text in the middle of the rectangle
2254
+ painter.drawText(int(text_x), int(text_y), text)
2255
+ except Exception:
2256
+ traceback.print_exc()
2257
+
2258
+ if restore_painter_in_the_end:
2259
+ painter.restore()
2260
+
2261
+ def _show_script_file(self, path):
2262
+ """Show the PDF/SVG file a panel script produced (e.g. latex(...)): the
2263
+ panel keeps its own filename, since the script makes the file again."""
2264
+ if not (os.path.isfile(path) and path.lower().endswith(('.pdf', '.svg'))):
2265
+ self.script_log = (self.script_log or '') + f'\nnot a PDF or SVG file: {path}'
2266
+ self.img = None
2267
+ return
2268
+ source = self.filename
2269
+ super(Image2D, self).__init__(0, 0, 0, 0)
2270
+ self.setFigure(path)
2271
+ self.filename = source
2272
+
2273
+ def setFigure(self, figure):
2274
+ self.must_update_figure_on_first_paint = False
2275
+
2276
+ if figure is not None and isinstance(figure, plt.Figure):
2277
+ if isinstance(getattr(self, 'img', None), plt.Figure) and self.img is not figure:
2278
+ plt.close(self.img)
2279
+ self.img = figure
2280
+ buffer = self._toBuffer(bufferType='svg')
2281
+ self.renderer = _make_svg_renderer(buffer.read())
2282
+ buffer.close()
2283
+ if self.width() == 0:
2284
+ size = self.renderer.defaultSize()
2285
+ self.setWidth(size.width())
2286
+ self.setHeight(size.height())
2287
+ elif figure is not None and isinstance(figure, str): # path to an svg or pdf file
2288
+ if figure.lower().endswith('.pdf'):
2289
+ try:
2290
+ # the page to show is the PDF's "series" (0-based), like a .lif series
2291
+ page = getattr(self, 'serie_to_open', None) or 0
2292
+ self._raw_svg_bytes = pdf_to_svg_bytes(figure, page=page)
2293
+ except Exception as e:
2294
+ logger.error('could not read PDF "' + str(figure) + '": ' + str(e))
2295
+ self._raw_svg_bytes = None
2296
+ self.isSet = False
2297
+ return
2298
+ # Qt's SVG renderer ignores clip paths: a dedicated renderer
2299
+ # keeps plot curves inside their axes
2300
+ from batoolset.drawings.pdf_svg_renderer import make_pdf_svg_renderer
2301
+ self.renderer = make_pdf_svg_renderer(self._raw_svg_bytes)
2302
+ try:
2303
+ self.renderer.setAspectRatioMode(Qt.AspectRatioMode.KeepAspectRatio)
2304
+ except AttributeError:
2305
+ pass
2306
+ self.renderer._pyf_svg_source = self._raw_svg_bytes # for svg_embed
2307
+ else:
2308
+ try:
2309
+ with open(figure, 'rb') as _f:
2310
+ self._raw_svg_bytes = _f.read()
2311
+ except Exception:
2312
+ self._raw_svg_bytes = None
2313
+ self.renderer = _make_svg_renderer(figure)
2314
+ self.img= self.renderer
2315
+ self.filename = figure
2316
+ if self.width() == 0:
2317
+ size = self.renderer.defaultSize()
2318
+ self.setWidth(size.width())
2319
+ self.setHeight(size.height())
2320
+ if size.width() <= 0:
2321
+ logger.error('image "' + str(self.filename) + '" could not be loaded')
2322
+ self.isSet = False
2323
+ return
2324
+ else:
2325
+ logger.error(
2326
+ 'The provided figure is not a valid matplotlib figure nor a valid svg file! Nothing can be done with it... Sorry...')
2327
+
2328
+ # what a zoom callout's inset copies from its parent (the display of the same pixels)
2329
+ _PARENT_DISPLAY_ATTRS = ('LUTs', 'channels', 'normalization', 'projection',
2330
+ 'projection_z_start', 'projection_z_end', 'dimensions',
2331
+ 'lower_indices', 'upper_indices', 'lower_values', 'upper_values')
2332
+
2333
+ def _sync_display_from_parent(self, parent):
2334
+ """For a zoom callout's inset (follow_parent_display): show the parent's
2335
+ pixels the way the parent shows them, also after the parent's LUTs,
2336
+ channels or display range change. Returns True if anything changed."""
2337
+ if not getattr(self, 'follow_parent_display', False):
2338
+ return False
2339
+ changed = False
2340
+ for attr in self._PARENT_DISPLAY_ATTRS:
2341
+ value = getattr(parent, attr, None)
2342
+ mine = getattr(self, attr, None)
2343
+ try:
2344
+ same = bool(np.array_equal(np.asarray(mine, dtype=object), np.asarray(value, dtype=object)))
2345
+ except Exception:
2346
+ same = repr(mine) == repr(value)
2347
+ if not same:
2348
+ setattr(self, attr, copy.deepcopy(value))
2349
+ changed = True
2350
+ if changed and getattr(self, 'img', None) is not None and not isinstance(self.img, QRectF):
2351
+ try:
2352
+ self.update_qimage()
2353
+ except Exception:
2354
+ traceback.print_exc()
2355
+ return changed
2356
+
2357
+ def _update_inset_scale(self, parent):
2358
+ """For an inset: set its scale so it is drawn
2359
+ fraction_of_parent_image_width_if_image_is_inset x the parent's displayed
2360
+ width (keeping its own cropped aspect ratio). Returns the inset's size as
2361
+ a QRectF at the origin. Must run before the inset is positioned."""
2362
+ rect = QRectF(0, 0, parent.width(all=True) * self.fraction_of_parent_image_width_if_image_is_inset, 0)
2363
+ cropped = self.getRect()
2364
+ if cropped.height() > 0 and rect.width() > 0:
2365
+ rect.setHeight(rect.width() / (cropped.width() / cropped.height()))
2366
+ self.set_to_scale(self.width() / rect.width())
2367
+ return rect
2368
+
2369
+ def draw_annotations(self,painter, draw=True, parent=None,restore_painter_in_the_end=True):
2370
+ try:
2371
+ extra_space = 1
2372
+ # we get the current transform and it should be applied to all objects but the placed objects to which no rotation should be applied
2373
+ current_transform = painter.worldTransform()
2374
+ empty_transform = copy_transform_without_rotation(current_transform) # QTransform() #
2375
+
2376
+ if self.annotations is not None and self.annotations:
2377
+ objects_grouped_by_position = groupby_position( self.annotations)
2378
+ for extra in self.annotations:
2379
+ if isinstance(extra, ScaleBar):
2380
+ extra.unit_to_pixel_conversion_factor = self.px_to_unit_conversion_factor
2381
+ extra.update_bar_at_scale(self.scale)
2382
+ if isinstance(extra, Image2D):
2383
+ extra._sync_display_from_parent(self)
2384
+ # size the inset BEFORE placing it: its scale used to be
2385
+ # set only while drawing, so placement used the stale
2386
+ # stored scale — a single-pass render (export) put a
2387
+ # cropped inset at the wrong corner offset and clipped it
2388
+ extra._update_inset_scale(self)
2389
+ try:
2390
+ try:
2391
+ pos = extra.placement.get_position()
2392
+ except Exception:
2393
+ pos = None
2394
+ if pos:
2395
+ # letters, labels, scale bars... keep a small gap from
2396
+ # the edges they are pinned to; an inset sits flush, and
2397
+ # so does a text on a filled box (the box left an ugly
2398
+ # gap to the image edge)
2399
+ boxed = isinstance(extra, TAText2D) and getattr(extra, 'fill_color', None) is not None
2400
+ m = 0 if isinstance(extra, Image2D) or boxed else EDGE_MARGIN
2401
+ if extra.placement.check_position('top'):
2402
+ extra.setTopLeft(extra.x(), self.topLeft().y() + m)
2403
+ if extra.placement.check_position('left'):
2404
+ extra.setTopLeft(self.topLeft().x() + m, extra.y())
2405
+ if extra.placement.check_position('right'):
2406
+ extra.setTopLeft(self.x() + self.width(all=True) - extra.width(all=True) - m, extra.y())
2407
+ if extra.placement.check_position('bottom'):
2408
+ extra.setTopLeft(extra.x(), self.topLeft().y() + self.height(all=True) - extra.height(
2409
+ all=True) - m)
2410
+ if extra.placement.check_position('center_h'):
2411
+ extra.setTopLeft(
2412
+ self.x() + self.width(all=True) / 2. - extra.width(all=True) / 2., extra.y())
2413
+ if extra.placement.check_position('center_v'):
2414
+ extra.setTopLeft(extra.x(), self.y() + self.height(all=True) / 2. - extra.height(
2415
+ all=True) / 2.)
2416
+ except Exception:
2417
+ traceback.print_exc()
2418
+
2419
+ # now for each group I need stack them --> give it a try
2420
+ for group, objects in objects_grouped_by_position.items():
2421
+ if group == ('free', 'relative'):
2422
+ continue
2423
+ else:
2424
+ pack2(extra_space, objects[0].placement, True,
2425
+ *objects) # TODO --> find packing mode from position --> should be doable --> and can ignore
2426
+
2427
+ for extra in self.annotations:
2428
+ rect_to_plot = self.getRect(all=True)
2429
+ center = rect_to_plot.center()
2430
+
2431
+ if ('free', 'relative') in objects_grouped_by_position and extra in objects_grouped_by_position[('free', 'relative')]:
2432
+ # THESE OBJECTS ARE NOT PLACED SO THEY SHOULD BE ROTATED
2433
+ painter.setWorldTransform(current_transform)
2434
+
2435
+ if self.theta and not (self.crop_left or self.crop_right or self.crop_top or self.crop_bottom):
2436
+ painter.translate(center.x(), center.y())
2437
+ painter.rotate(self.theta)
2438
+ painter.translate(-center.x(), -center.y())
2439
+ painter.translate(self.getRect(all=True).center()-self.getRect(scale=True).center()) # this is required for rotated shapes
2440
+
2441
+ if not self.theta and (self.crop_left or self.crop_right or self.crop_top or self.crop_bottom):
2442
+ # this fixes the stuff for rotation only but not for translation --> I'm almost there but not fully yet
2443
+ tmp = get_shape_after_rotation_and_crop(self.getRect(raw=True), self.theta, self.crop_left,
2444
+ self.crop_right, self.crop_top, self.crop_bottom)
2445
+
2446
+ parent = self.getRect(raw=True)
2447
+ parent = QRectF(0, 0, parent.width(), parent.height())
2448
+
2449
+ if tmp is not None:
2450
+ tmp2 = parent.topLeft() - QRectF(tmp.boundingRect()).topLeft()
2451
+ painter.translate(tmp2.x() / self.scale, tmp2.y() / self.scale)
2452
+
2453
+ if self.theta and (self.crop_left or self.crop_right or self.crop_top or self.crop_bottom):
2454
+ # painter.setClipping(False)
2455
+
2456
+ # TODO --> maybe code this below in getRect(raw_no_crop) for example
2457
+ tmp_crop_left = self.crop_left
2458
+ tmp_crop_right = self.crop_right
2459
+ tmp_crop_top = self.crop_top
2460
+ tmp_crop_bottom = self.crop_bottom
2461
+
2462
+ self.crop_left =0
2463
+ self.crop_right=0
2464
+ self.crop_top=0
2465
+ self.crop_bottom=0
2466
+
2467
+ rect_uncropped = self.getRect(all=True)
2468
+ rect_uncropped.translate(rect_to_plot.center()-rect_uncropped.center())
2469
+ # rect_uncropped.translate(rect_to_plot.topLeft()-rect_uncropped.topLeft())
2470
+ center = rect_uncropped.center()
2471
+
2472
+ self.crop_left = tmp_crop_left
2473
+ self.crop_right=tmp_crop_right
2474
+ self.crop_top=tmp_crop_top
2475
+ self.crop_bottom=tmp_crop_bottom
2476
+
2477
+ extra_user_translation, extra_translation_at_painter = compute_translation_corrections(self, minimal_return=True)
2478
+ painter.translate(extra_translation_at_painter/self.scale)
2479
+
2480
+ painter.translate(center.x(), center.y())
2481
+ painter.rotate(self.theta)
2482
+ painter.translate(-center.x(), -center.y())
2483
+
2484
+ if extra_user_translation:
2485
+ try:
2486
+ painter.translate(extra_user_translation[0]/self.scale, extra_user_translation[1]/self.scale)
2487
+ except Exception:
2488
+ traceback.print_exc()
2489
+ else:
2490
+ if extra.placement is None or not extra.placement.position_to_string():
2491
+ # THIS OBJECT IS NOT PLACED SO IT SHOULD BE ROTATED
2492
+ painter.setWorldTransform(current_transform)
2493
+ if self.theta:
2494
+ painter.translate(center.x(), center.y())
2495
+ painter.rotate(self.theta)
2496
+ painter.translate(-center.x(), -center.y())
2497
+ painter.translate(self.getRect(all=True).center() - self.getRect(scale=True).center()) # this is required for rotated shapes
2498
+ else:
2499
+ # THIS OBJECT IS PLACED AND THEREFORE SHOULD NOT BE ROTATED
2500
+ painter.setWorldTransform(empty_transform)
2501
+
2502
+ extra.draw(painter=painter, parent=self) # added parent for relative plotting
2503
+ except Exception:
2504
+ traceback.print_exc()
2505
+
2506
+ if restore_painter_in_the_end:
2507
+ painter.restore()
2508
+ def draw(self, painter, draw=True, parent=None):
2509
+ if draw:
2510
+ # TODO --> shall I recover and reuse the rect ???
2511
+
2512
+ self.draw_bg(painter, draw=draw, parent=parent, restore_painter_in_the_end=False)
2513
+
2514
+ # letter is good
2515
+ # then position all of them as a chain --> TODO
2516
+ self.draw_annotations(painter, draw=draw, parent=parent, restore_painter_in_the_end=False)
2517
+
2518
+ painter.restore()
2519
+
2520
+ def setToWidth(self, width_in_px):
2521
+ pure_image_width = self.width(scaled=False)# need original height and with in fact
2522
+ scale = width_in_px / pure_image_width
2523
+ self.scale = scale
2524
+
2525
+ def setToHeight(self, height_in_px):
2526
+ pure_image_height = self.height(scaled=False)
2527
+ scale = height_in_px/pure_image_height
2528
+ self.scale = scale
2529
+
2530
+ def crop(self, left=None, right=None, top=None, bottom=None, all=None):
2531
+ if left is not None:
2532
+ self.crop_left = left
2533
+ if right is not None:
2534
+ self.crop_right = right
2535
+ if top is not None:
2536
+ self.crop_top = top
2537
+ if bottom is not None:
2538
+ self.crop_bottom = bottom
2539
+ if all is not None:
2540
+ self.crop_left = all
2541
+ self.crop_right = all
2542
+ self.crop_top = all
2543
+ self.crop_bottom = all
2544
+ def set_to_translation(self, translation):
2545
+ self.translation = translation
2546
+
2547
+ def boundingRect(self, scaled=True):
2548
+ # en fait pas good besoin de prendre les crops et le scale en compte
2549
+ # is
2550
+ # rect_to_plot = self.adjusted(self.crop_left, self.crop_top, -self.crop_right, -self.crop_bottom)
2551
+ rect_to_plot = self.adjusted(0, 0, -self.crop_right-self.crop_left, -self.crop_bottom-self.crop_top)
2552
+ # rect_to_plot = self.adjusted(-self.crop_left, -self.crop_top, -self.crop_right, -self.crop_bottom)
2553
+ # rect_to_plot = self.adjusted(0,0,0,0)
2554
+ # print('begin rect_to_plot', rect_to_plot, self.scale)
2555
+ # if kwargs['draw']==True or kwargs['fill']==True:
2556
+ # if self.scale is None or self.scale==1:
2557
+ # painter.drawRect(self)
2558
+ # else:
2559
+ # on clone le rect
2560
+ if self.scale is not None and self.scale != 1 and scaled:
2561
+ # TODO KEEP THE ORDER THIS MUST BE DONE THIS WAY OR IT WILL GENERATE PLENTY OF BUGS...
2562
+ new_width = rect_to_plot.width() * self.scale
2563
+ new_height = rect_to_plot.height() * self.scale
2564
+ # print(rect_to_plot.width(), rect_to_plot.height()) # here ok
2565
+ # setX changes width --> why is that
2566
+
2567
+ # TODO BE EXTREMELY CAREFUL AS SETX AND SETY CAN CHANGE WIDTH AND HEIGHT --> ALWAYS TAKE SIZE BEFORE OTHERWISE THERE WILL BE A PB AND ALWAYS RESET THE SIZE WHEN SETX IS CALLED!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
2568
+ # Sets the left edge of the rectangle to the given x coordinate. May change the width, but will never change the right edge of the rectangle. --> NO CLUE WHY SHOULD CHANGE WIDTH THOUGH BUT BE CAREFUL!!!
2569
+ # rect_to_plot.setX(rect_to_plot.x() * self.scale)
2570
+ # rect_to_plot.setY(rect_to_plot.y() * self.scale)
2571
+ # maybe to avoid bugs I should use translate instead rather that set x but ok anyways
2572
+ # print(rect_to_plot.width(), rect_to_plot.height())# bug here --> too big
2573
+
2574
+ # print(new_height, new_height, self.width(), self.scale, self.scale* self.width())
2575
+ rect_to_plot.setWidth(new_width)
2576
+ rect_to_plot.setHeight(new_height)
2577
+ return rect_to_plot
2578
+
2579
+ SVG_INKSCAPE = 96
2580
+ SVG_ILLUSTRATOR = 72
2581
+
2582
+ qualities = [QPainter.SmoothPixmapTransform, QPainter.TextAntialiasing, None, QPainter.Antialiasing] # None sets antialiasing to False -−> see code after # painter.setRenderHint QPainter.HighQualityAntialiasing,
2583
+
2584
+ '''
2585
+ Here are some common interpolation modes:
2586
+
2587
+ Qt.SmoothPixmapTransform: This mode enables smooth transformation of pixmaps when they are scaled or rotated. It uses bilinear or bicubic interpolation to provide smoother results, which can be useful when resizing images or drawing rotated content. However, this mode may have a performance impact, especially when dealing with large images or complex transformations.
2588
+ Qt.HighQualityAntialiasing: This mode enables high-quality antialiasing for painting operations. Antialiasing helps to reduce the jagged appearance of diagonal lines and curves by smoothing their edges. This mode can be useful when drawing shapes, text, or other graphical elements that require smooth edges. However, it may also have a performance impact, especially when dealing with complex scenes or large-scale drawings.
2589
+ Qt.TextAntialiasing: This mode enables antialiasing specifically for text rendering. It can help to improve the readability and appearance of text in your application, especially at small font sizes or when using custom fonts. However, it may also have a performance impact, especially when dealing with large amounts of text or complex text layouts.
2590
+
2591
+ painter = QPainter(some_widget)
2592
+ painter.setRenderHint(Qt.SmoothPixmapTransform)
2593
+ # or
2594
+ painter.setRenderHint(Qt.HighQualityAntialiasing)
2595
+ # or
2596
+ painter.setRenderHint(Qt.TextAntialiasing)
2597
+
2598
+ painter.setRenderHints(Qt.SmoothPixmapTransform | Qt.HighQualityAntialiasing)
2599
+ '''
2600
+ def save(self, path, filetype=None, title=None, description=None, svg_dpi=SVG_INKSCAPE, quality=qualities[-1]):
2601
+ # if path is None or not isinstance(path, str):
2602
+ # logger.error('please provide a valide path to save the image "' + str(path) + '"')
2603
+ # return
2604
+ if path is None:
2605
+ filetype = '.tif'
2606
+
2607
+ if filetype is None:
2608
+ if path.lower().endswith('.svg'):
2609
+ filetype = 'svg'
2610
+ else:
2611
+ filetype = os.path.splitext(path)[1]
2612
+ dpi = 72 # 300 # inkscape 96 ? check for illustrator --> check
2613
+
2614
+ if filetype == 'svg':
2615
+ generator = QSvgGenerator()
2616
+ generator.setFileName(path)
2617
+ if svg_dpi == self.SVG_ILLUSTRATOR:
2618
+ generator.setSize(QSize(595, 842))
2619
+ generator.setViewBox(QRect(0, 0, 595, 842))
2620
+ else:
2621
+ generator.setSize(QSize(794, 1123))
2622
+ generator.setViewBox(QRect(0, 0, 794, 1123))
2623
+
2624
+ if title is not None and isinstance(title, str):
2625
+ generator.setTitle(title)
2626
+ if description is not None and isinstance(description, str):
2627
+ generator.setDescription(description)
2628
+ generator.setResolution(
2629
+ svg_dpi) # fixes issues in inkscape of pt size --> 72 pr illustrator and 96 pr inkscape but need change size
2630
+
2631
+ painter = QPainter(generator)
2632
+
2633
+ # print(generator.title(), generator.heightMM(), generator.height(), generator.widthMM(),
2634
+ # generator.resolution(), generator.description(), generator.logicalDpiX())
2635
+ else:
2636
+ scaling_factor_dpi = 1
2637
+ # scaling_factor_dpi = self.scaling_factor_to_achieve_DPI(300)
2638
+
2639
+ # in fact take actual page size ??? multiplied by factor
2640
+ # just take real image size instead
2641
+
2642
+
2643
+ # image = QtGui.QImage(QSize(self.cm_to_inch(21) * dpi * scaling_factor_dpi, self.cm_to_inch(29.7) * dpi * scaling_factor_dpi), QtGui.QImage.Format_RGBA8888) # minor change to support alpha # QtGui.QImage.Format_RGB32)
2644
+
2645
+ # NB THE FOLLOWING LINES CREATE A WEIRD ERROR WITH WEIRD PIXELS DRAWN some sort of lines NO CLUE WHY
2646
+
2647
+ img_bounds = self.boundingRect()
2648
+ # image = QtGui.QImage(QSize(img_bounds.width() * scaling_factor_dpi, img_bounds.height()* scaling_factor_dpi), QtGui.QImage.Format_RGBA8888) # minor change to support alpha # QtGui.QImage.Format_RGB32)
2649
+ image = QtGui.QImage(QSize(int(img_bounds.width() * scaling_factor_dpi), int(img_bounds.height()* scaling_factor_dpi)), QtGui.QImage.Format_RGBA8888) # minor change to support alpha # QtGui.QImage.Format_RGB32)
2650
+ # image = QtGui.QImage(QSize(int(img_bounds.scale(scaling_factor_dpi))), QtGui.QImage.Format_RGBA8888) # minor change to support alpha # QtGui.QImage.Format_RGB32)
2651
+ # print('size at dpi',QSize(img_bounds.width() * scaling_factor_dpi, img_bounds.height()* scaling_factor_dpi))
2652
+ # QSize(self.cm_to_inch(0.02646 * img_bounds.width())
2653
+ # self.cm_to_inch(0.02646 * img_bounds.height())
2654
+ # need convert pixels to inches
2655
+ # is there a rounding error
2656
+
2657
+ # force white bg for non jpg
2658
+ try:
2659
+ # print(filetype.lower())
2660
+ # the tif and png file formats support alpha
2661
+ if not filetype.lower() == '.png' and not filetype.lower() == '.tif' and not filetype.lower() == '.tiff':
2662
+ image.fill(QColor.fromRgbF(1,1,1))
2663
+ else:
2664
+ # image.fill(QColor.fromRgbF(1, 1, 1, alpha=1))
2665
+ # image.fill(QColor.fromRgbF(1, 1, 1, alpha=1))
2666
+ # TODO KEEP in fact image need BE FILLED WITH TRANSPARENT OTHERWISE GETS WEIRD DRAWING ERRORS
2667
+ # TODO KEEP SEE https://stackoverflow.com/questions/13464627/qt-empty-transparent-qimage-has-noise
2668
+ # image.fill(qRgba(0, 0, 0, 0))
2669
+ image.fill(QColor.fromRgbF(0,0,0,0))
2670
+ except Exception:
2671
+ pass
2672
+ painter = QPainter(image) # see what happens in case of rounding of pixels
2673
+ # painter.begin()
2674
+ painter.scale(scaling_factor_dpi, scaling_factor_dpi)
2675
+ if quality is not None:
2676
+ painter.setRenderHint(quality) # to improve rendering quality
2677
+ else:
2678
+ painter.setRenderHint(QPainter.Antialiasing, False) # to improve rendering quality
2679
+ self.enter_export_mode()
2680
+ try:
2681
+ self.draw(painter)
2682
+ finally:
2683
+ self.exit_export_mode()
2684
+ painter.end()
2685
+ if path is None:
2686
+ return image
2687
+ if filetype != 'svg':
2688
+ image.save(path)
2689
+ return image
2690
+
2691
+ #based on https://stackoverflow.com/questions/19902183/qimage-to-numpy-array-using-pyside
2692
+ def convert_qimage_to_numpy(self, qimage):
2693
+ qimage = qimage.convertToFormat(QtGui.QImage.Format.Format_RGB32)
2694
+ width = qimage.width()
2695
+ height = qimage.height()
2696
+ image_pointer = qimage.bits() # creates a deep copy --> this is what I want
2697
+ # image_pointer.setsize(qimage.byteCount())
2698
+ try:
2699
+ image_pointer.setsize(qimage.sizeInBytes()) # qt6 version of the stuff
2700
+ except Exception:
2701
+ image_pointer.setsize(qimage.byteCount())
2702
+ # arr = np.array(image_pointer,copy=True).reshape(height, width, 4)
2703
+ arr = np.array(image_pointer).reshape(height, width, 4)
2704
+ arr = arr[..., 0:3]
2705
+ arr = RGB_to_BGR(arr) # that seems to do the job
2706
+ return arr
2707
+
2708
+ def draw_inner_layout_selection(self, painter):
2709
+ painter.save()
2710
+ try:
2711
+ rect = self.getRect(all=True) # there is a big bug with that
2712
+ painter.setPen(QPen(QColor(0, 255, 255)))
2713
+ painter.drawRect(rect)
2714
+ except Exception:
2715
+ print('inner error')
2716
+ traceback.print_exc()
2717
+ painter.restore()
2718
+
2719
+ def width(self, *args, **kwargs):
2720
+ # this should be uncropped and unrotated in fact or rotated in fact and uncropped ??? -−> think about it
2721
+ if args or kwargs:
2722
+ return self.getRect(all=True).width() # this is probably incorrect but ok for now
2723
+ else:
2724
+ return self.getRect().width() #* (self.scale if self.scale else 1)
2725
+
2726
+ def height(self, *args, **kwargs):
2727
+ if args or kwargs:
2728
+ return self.getRect(all=True).height() # this is probably incorrect but ok for now
2729
+ else:
2730
+ return self.getRect().height()# * (self.scale if self.scale else 1)
2731
+
2732
+ def getRect(self, *args, **kwargs):
2733
+ # w = super().width()
2734
+ # h = super().height()
2735
+ x = self.x()
2736
+ y = self.y()
2737
+ # if isinstance(self.img, np.ndarray):
2738
+ # w = self.img.shape[1]
2739
+ # h = self.img.shape[0]
2740
+ # elif isinstance(self.img, plt.Figure):
2741
+ # rect = get_fig_rect(self.img)
2742
+ # w = rect.width()
2743
+ # h = rect.height()
2744
+ # elif isinstance(self.renderer, QSvgRenderer):
2745
+ # size = self.renderer.defaultSize()
2746
+ # w = size.width()
2747
+ # h = size.height()
2748
+ w, h = self.get_raw_size()
2749
+
2750
+ # if self.is_template():
2751
+ # return QRectF(x,y,w,h)
2752
+
2753
+ if args or kwargs:
2754
+
2755
+ if 'raw' in kwargs:
2756
+ return QRectF(x,y,w,h)
2757
+ if 'all' in kwargs:
2758
+ rect_to_plot = QRectF(x, y, w, h)
2759
+
2760
+
2761
+ center = rect_to_plot.center()
2762
+
2763
+ if True: # try to correct the stuff in fact not really working --> see how I can fix that
2764
+ tmp = QRectF(rect_to_plot)
2765
+ if self.crop_left:
2766
+ tmp.setWidth(tmp.width() - self.crop_left)
2767
+ if self.crop_right:
2768
+ tmp.setWidth(tmp.width() - self.crop_right)
2769
+ if self.crop_top:
2770
+ tmp.setHeight(tmp.height() - self.crop_top)
2771
+ if self.crop_bottom:
2772
+ tmp.setHeight(tmp.height() - self.crop_bottom)
2773
+ tmp.setWidth(tmp.width() / self.scale)
2774
+ tmp.setHeight(tmp.height() / self.scale)
2775
+
2776
+ if self.theta and not self.theta == 0: # with that the rotation is ok but the position of the rect is not --> but with angle 0 it is --> can I correct for the translation
2777
+ t = QTransform().translate(center.x(), center.y()).rotate(self.theta).translate(-center.x(),
2778
+ -center.y()) # .translate(center.x(), center.y())
2779
+ rotatedPolygon = t.mapToPolygon(rect_to_plot.toRect()) # Map the rectangle to a rotated polygon
2780
+ # Create a QRectF from the bounding rectangle of the rotated polygon
2781
+ rotatedRect = QRectF(rotatedPolygon.boundingRect())
2782
+ else:
2783
+ if self.crop_left:
2784
+ rect_to_plot.setWidth(rect_to_plot.width() - self.crop_left)
2785
+ if self.crop_right:
2786
+ rect_to_plot.setWidth(rect_to_plot.width() - self.crop_right)
2787
+ if self.crop_top:
2788
+ rect_to_plot.setHeight(rect_to_plot.height() - self.crop_top)
2789
+ if self.crop_bottom:
2790
+ rect_to_plot.setHeight(rect_to_plot.height() - self.crop_bottom)
2791
+ rect_to_plot.setWidth(rect_to_plot.width() / self.scale)
2792
+ rect_to_plot.setHeight(rect_to_plot.height() / self.scale)
2793
+
2794
+ return rect_to_plot
2795
+ try:
2796
+ rotatedRect = QRectF(rotatedRect.boundingRect())
2797
+
2798
+ except Exception:
2799
+ pass
2800
+
2801
+ if self.crop_left:
2802
+ rotatedRect.setWidth(rotatedRect.width() - self.crop_left)
2803
+ if self.crop_right:
2804
+ rotatedRect.setWidth(rotatedRect.width() - self.crop_right)
2805
+ if self.crop_top:
2806
+ rotatedRect.setHeight(rotatedRect.height() - self.crop_top)
2807
+ if self.crop_bottom:
2808
+ rotatedRect.setHeight(rotatedRect.height() - self.crop_bottom)
2809
+
2810
+ rotatedRect.setWidth(rotatedRect.width() / self.scale)
2811
+ rotatedRect.setHeight(rotatedRect.height() / self.scale)
2812
+ rotatedRect.translate(tmp.topLeft() - rotatedRect.topLeft())
2813
+
2814
+ return rotatedRect
2815
+ else:
2816
+ rect_to_plot = QRectF(self.x(), self.y(), w, h)
2817
+ if self.crop_left:
2818
+ rect_to_plot.setWidth(rect_to_plot.width() - self.crop_left)
2819
+ if self.crop_right:
2820
+ rect_to_plot.setWidth(rect_to_plot.width() - self.crop_right)
2821
+ if self.crop_top:
2822
+ rect_to_plot.setHeight(rect_to_plot.height() - self.crop_top)
2823
+ if self.crop_bottom:
2824
+ rect_to_plot.setHeight(rect_to_plot.height() - self.crop_bottom)
2825
+ rect_to_plot.setWidth(rect_to_plot.width() / self.scale)
2826
+ rect_to_plot.setHeight(rect_to_plot.height() / self.scale)
2827
+ return rect_to_plot
2828
+ else:
2829
+ rect_to_plot = QRectF(self.x(), self.y(), w, h)
2830
+ center = rect_to_plot.center()
2831
+ rotatedRect = rect_to_plot
2832
+ if self.theta:
2833
+ t = QTransform().translate(center.x(), center.y()).rotate(self.theta).translate(-center.x(),
2834
+ -center.y())
2835
+ rotatedPolygon = t.mapToPolygon(rect_to_plot.toRect()) # Map the rectangle to a rotated polygon
2836
+ # Create a QRectF from the bounding rectangle of the rotated polygon
2837
+ rotatedRect = QRectF(rotatedPolygon.boundingRect())
2838
+ if self.crop_left:
2839
+ rotatedRect.setWidth(rotatedRect.width() - self.crop_left)
2840
+ if self.crop_right:
2841
+ rotatedRect.setWidth(rotatedRect.width() - self.crop_right)
2842
+ if self.crop_top:
2843
+ rotatedRect.setHeight(rotatedRect.height() - self.crop_top)
2844
+ if self.crop_bottom:
2845
+ rotatedRect.setHeight(rotatedRect.height() - self.crop_bottom)
2846
+ try:
2847
+ return QRectF(rotatedRect.boundingRect())
2848
+ except Exception:
2849
+ return rotatedRect
2850
+
2851
+ # def contains(self, *__args):
2852
+ # try:
2853
+ # # if self.__sel:
2854
+ # # return self.__sel.contains(*__args)
2855
+ # if __args is not None and __args:
2856
+ # return self.getRect(all=True).contains(*__args)
2857
+ # else:
2858
+ # return False
2859
+ # # return self.getRect(scale=True).contains(*__args)
2860
+ # except:
2861
+ # # traceback.print_exc() # somehow this generates an error but does it make sense
2862
+ # # I have too many handling of rotations --> that may cause trouble too
2863
+ # # simplest is to really make the rect as it should be and as selection should be !!!
2864
+ # pass
2865
+ # return False
2866
+
2867
+
2868
+ def __contains__(self, item):
2869
+ return False
2870
+
2871
+ def __copy__(self):
2872
+ return self # TODO --> rather do a clone of self
2873
+
2874
+ def __deepcopy__(self, memo):
2875
+ return self # TODO --> rather do a clone of self
2876
+
2877
+ # DEV NOTE KEEP be extremely careful with that because this is what is compared when using == !!! so if poorly implemented this can have dramatic effects
2878
+ def __str__(self):
2879
+ return self.__repr__()
2880
+
2881
+ def __repr__(self):
2882
+ class_name = type(self).__name__
2883
+ memory_address = hex(id(self))
2884
+
2885
+ # print('debugggging', self.img, self.custom_loading_script, ((self.img is None or isinstance(self.img, QRectF)) and self.filename is not None), ((self.img is None or isinstance(self.img, QRectF)) and self.custom_loading_script))
2886
+
2887
+ if self.isText:
2888
+ class_name = 'Label_' + class_name
2889
+ elif self.is_template():
2890
+ class_name = 'Template_' + class_name
2891
+ # elif (self.img is None and self.filename is not None) or (self.img is None and self.custom_loading_script):
2892
+ elif ((self.img is None or isinstance(self.img, QRectF)) and self.filename is not None) or ((self.img is None or isinstance(self.img, QRectF)) and self.custom_loading_script):
2893
+ class_name = 'Error_' + class_name
2894
+ elif isinstance(self.img, plt.Figure):
2895
+ class_name = 'Fig_' + class_name
2896
+ elif self.renderer is not None:
2897
+ class_name = 'SVG_'+class_name
2898
+ elif self.img is None:
2899
+ class_name = 'Empty_'+class_name
2900
+
2901
+ if self.filename is not None:
2902
+ base_name = os.path.basename(self.filename)
2903
+ return f"{class_name}-{memory_address}-{base_name}"
2904
+ return f"{class_name}-{memory_address}"
2905
+
2906
+ def to_dict(self):
2907
+ x = self.x()
2908
+ y = self.y()
2909
+ # Store the RAW (uncropped, unrotated) size: every loader reads width /
2910
+ # height back as the raw image size and applies crop/rotation on top.
2911
+ # self.width() is the cropped+rotated extent, so storing it made each
2912
+ # save/lazy-load cycle subtract the crop again (160 -> 120 -> 80 px).
2913
+ try:
2914
+ width, height = self.get_raw_size()
2915
+ except Exception:
2916
+ width, height = self.width(), self.height()
2917
+
2918
+ # Create a dictionary representation of the values of the super object
2919
+ output_dict = {
2920
+ 'x': x,
2921
+ 'y': y,
2922
+ 'width': width,
2923
+ 'height': height,
2924
+ # 'args':self.filename,
2925
+ 'filename':self.filename,
2926
+ }
2927
+ # Update the dictionary with the __dict__ of Rectangle2D
2928
+ output_dict.update(self.__dict__)
2929
+ try:
2930
+ output_dict['extra_user_translation']=list(self.extra_user_translation) #
2931
+ except Exception:
2932
+ pass
2933
+ try:
2934
+ del output_dict['script_log']
2935
+ except Exception:
2936
+ pass
2937
+ try:
2938
+ del output_dict['_display_cache'] # runtime-only pixmap cache, never serialize
2939
+ except Exception:
2940
+ pass
2941
+ try:
2942
+ del output_dict['_qimage_proxy'] # runtime-only downsampled display copy, never serialize
2943
+ except Exception:
2944
+ pass
2945
+ if isinstance(self.img, QRectF):
2946
+ output_dict['template_rect']=self.img
2947
+ output_dict['LUTs']=str(self.LUTs)
2948
+ output_dict['channels']=str(self.channels)
2949
+ output_dict['lower_indices']=None
2950
+ output_dict['upper_indices']=None
2951
+ output_dict['lower_values']=None
2952
+ output_dict['upper_values']=None
2953
+ # output_dict['theta']=self.theta
2954
+ return output_dict
2955
+
2956
+ def execute_code_unsafe(self, code, parent=None): # TODO --> see how to handle the security!!!
2957
+ # Save the original streams
2958
+ orig_stdout = sys.stdout
2959
+ orig_stderr = sys.stderr
2960
+
2961
+ # Create temporary streams
2962
+ temp_stdout = io.StringIO()
2963
+ temp_stderr = io.StringIO()
2964
+
2965
+ # Redirect the streams to the temporary buffers
2966
+ sys.stdout = temp_stdout
2967
+ sys.stderr = temp_stderr
2968
+
2969
+ # If an exception occurs, capture it and include it in the output
2970
+ local_namespace = {'plt': plt, 'Img': Img, 'np':np} # all imports should be made there (otherwise the user needs to do the imports himslef --> painful though
2971
+ # PyFigures' helpers (latex(code) -> a vector panel), when it is installed
2972
+ try:
2973
+ from pyfigures.script_helpers import HELPERS
2974
+ local_namespace.update(HELPERS)
2975
+ except ImportError:
2976
+ pass
2977
+ if isinstance(code, str) and code.lstrip().startswith(('\\', '%')):
2978
+ # the script is LaTeX code itself (Python never starts like that)
2979
+ code = 'self.img = latex(' + repr(code) + ')'
2980
+ try:
2981
+ if self.filename and (self.filename.strip().startswith("['") and self.filename.strip().endswith("']")):
2982
+ self.filename = reload_string_list(self.filename)
2983
+ # the panel's source file, as `file_path` — the flip/rotate scripts
2984
+ # written by the MCP transform_image read it (without it every
2985
+ # flipped panel failed with NameError and showed nothing)
2986
+ local_namespace['file_path'] = self.filename
2987
+ exec(code, locals(), local_namespace)
2988
+ if 'self.img' in local_namespace:
2989
+ self.img = local_namespace['self.img'] # shall I transfer more things ? -−> maybe !!!
2990
+ except Exception as e:
2991
+ print("Error:", e)
2992
+ temp_stderr.write(f"An error occurred: {str(e)}\n")
2993
+ temp_stderr.write(f"{traceback.format_exc()}\n")
2994
+
2995
+ # Restore the original streams
2996
+ sys.stdout = orig_stdout
2997
+ sys.stderr = orig_stderr
2998
+
2999
+ # Return the captured output
3000
+ # return temp_stdout.getvalue(), temp_stderr.getvalue()
3001
+ # we redirect the log and maybe the output to a file
3002
+ self.script_log = temp_stderr.getvalue() if temp_stderr.getvalue() else temp_stdout.getvalue()
3003
+ # print('#'*20)
3004
+ # print('self.script_log', self.script_log)
3005
+ # print('-' * 20)
3006
+ # print('dabu', temp_stdout.getvalue())
3007
+ # print('%' * 20)
3008
+ # print('dabi',temp_stderr.getvalue())
3009
+ # print('#' * 20)
3010
+
3011
+ # Close the temporary streams
3012
+ temp_stdout.close()
3013
+ temp_stderr.close()
3014
+
3015
+ if self.filename:
3016
+ if not isinstance(self.filename, str) and isinstance(self.filename, list):
3017
+ self.filename= str(self.filename)
3018
+
3019
+
3020
+ if __name__ == '__main__':
3021
+ app = QApplication(sys.argv)# IMPORTANT KEEP !!!!!!!!!!!
3022
+ import sys
3023
+
3024
+ # can I try that ???
3025
+
3026
+ if True:
3027
+ img1 = Image2D('/E/Sample_images/counter/01.png', user_comments='comments added by the user test')
3028
+ print('before rotation',img1.getRect(), img1.getRect(scale=True), img1.getRect(all=True))
3029
+
3030
+ img1.crop(10,15,20,30)
3031
+ print('after crop', img1.getRect(), img1.getRect(scale=True), img1.getRect(all=True)) # -> all is ok --> so maybe just the drawing is fucked in fact now
3032
+
3033
+ img1.crop(0, 0, 0, 0)
3034
+ img1.set_rotation(45)
3035
+ print('after rotation',img1.getRect(), img1.getRect(scale=True), img1.getRect(all=True)) # there is a bug in scale because it does not apply rotation --> probably not correct
3036
+
3037
+ img1.crop(10,15,20,30)
3038
+ print('after rotation and crop', img1.getRect(), img1.getRect(scale=True), img1.getRect(all=True)) # there is a bug i
3039
+ sys.exit(0)
3040
+
3041
+ if True:
3042
+ # Data for plotting
3043
+ t = np.arange(0.0, 2.0, 0.01)
3044
+ s = 1 + np.sin(2 * np.pi * t)
3045
+
3046
+ fig, ax = plt.subplots()
3047
+ print(type(fig))
3048
+ ax.plot(t, s)
3049
+
3050
+ ax.set(xlabel='time (s)', ylabel='voltage (mV)', title=None)
3051
+ ax.grid()
3052
+
3053
+ figure = Image2D(fig)
3054
+
3055
+
3056
+
3057
+ sys.exit(0)
3058
+
3059
+ if True:
3060
+ # todo test the use of an empty image with given dimensions then once this is done try to do stuff
3061
+ # in fact in all cases the only thing that matters is the initial rect --> I could put a rect instead of an image and that would give me all the things I need
3062
+ # TODO
3063
+
3064
+ # try to do that
3065
+ try:
3066
+ empty_img = Image2D()
3067
+ print(empty_img.width())
3068
+ except Exception:
3069
+ traceback.print_exc()
3070
+ print('test that empty images are not allowed')
3071
+
3072
+ # do I already have a construction that would do the job
3073
+ empty_img = Image2D(width=120, height=240)# that seems to work --> but super is never called --> see
3074
+ # maybe allow an image to be initialized by a rect (to act as an empty image and if that is the case the image could be replaced) by what I need !!!
3075
+
3076
+ print(empty_img)
3077
+ print(empty_img.width())
3078
+
3079
+ sys.exit(0)
3080
+
3081
+ # is it possible that I never implemented the rotation ???
3082
+ # can I do it???
3083
+
3084
+ # VERY GOOD --> I HAVE FINALLY FIXED IT
3085
+
3086
+ # ça marche --> voici deux examples de shapes
3087
+ test = Image2D(x=12, y=0, width=100, height=100) # could also be used to create empty image with
3088
+
3089
+ print(test.img)
3090
+ print(test.boundingRect())
3091
+ # print(test.get_P1().x())
3092
+
3093
+ # bug qd on definit une image comme param
3094
+ # test = Image2D('/E/Sample_images/counter/06.png')
3095
+ test = Image2D('/E/Sample_images/counter/01.png')
3096
+ print(test.boundingRect()) # --> it is ok there so why not below # not callable --> why -->
3097
+ # print(test.get_P1()) # ça marche donc où est le bug
3098
+ # print(test.get_P1().y()) # ça marche donc où est le bug
3099
+ # print(test.getP1().width())
3100
+ # ça marche
3101
+
3102
+ # try draw on the image the quivers
3103
+ # img0.setLettering('<font color="red">A</font>')
3104
+ # # letter
3105
+ # img0.annotation.append(Rect2D(88, 88, 200, 200, stroke=3, color=0xFF00FF))
3106
+ # img0.annotation.append(Ellipse2D(88, 88, 200, 200, stroke=3, color=0x00FF00))
3107
+ # img0.annotation.append(Circle2D(33, 33, 200, stroke=3, color=0x0000FF))
3108
+
3109
+ test.annotations.append(TAText2D('<font color="red">A</font>', placement='top-left'))
3110
+
3111
+ test.annotations.append(Line2D(33, 33, 88, 88, stroke=3, color=0x0000FF))
3112
+
3113
+ test.annotations.append(Line2D(128, 33, 88, 88, stroke=0.65, color=0xFFFF00))
3114
+ # img0.annotation.append(Freehand2D(10, 10, 20, 10, 20, 30, 288, 30, color=0xFFFF00, stroke=3))
3115
+ # # img0.annotation.append(PolyLine2D(10, 10, 20, 10, 20, 30, 288, 30, color=0xFFFF00, stroke=3))
3116
+ # img0.annotation.append(Point2D(128, 128, color=0xFFFF00, stroke=6))
3117
+
3118
+ # add a save to the image --> so that it exports as a raster --> TODO
3119
+
3120
+
3121
+ # painter =
3122
+ # img = test.draw() # if no painter --> create one as not to lose any data and or allow to save as vectorial
3123
+
3124
+
3125
+ img = test.save('/E/trash/test_line2D.tif')
3126
+
3127
+ # test.save('/E/Sample_images/sample_images_PA/mini_vide/analyzed/trash/test_line2D.svg')
3128
+
3129
+ #trop facile --> just hack it so that it can return a single qimage # or return a numpy image that is then plotted -> should not be too hard !!! I think --> TODO
3130
+ img = test.convert_qimage_to_numpy(img) # --> ok but I just need to swap the channels then I'll be done --> try that maybe with plenty of input images just to see
3131
+
3132
+ # empty image work --> now try to see if I can create an image from a Figure and or a vector graphic ??? -−> TODO
3133
+
3134
+ # shall I save
3135
+ # try with non RGB images just to see
3136
+
3137
+ # img = RGB_to_BGR(img)
3138
+
3139
+ # almost there --> just need to check that the size of the image is ok and that everything is fine
3140
+ plt.imshow(img)
3141
+ plt.show()
3142
+
3143
+
3144
+ print('here')
3145
+ img2 = Image2D('/E/Sample_images/counter/02.png')
3146
+ print('here3')
3147
+ preview(img2)
3148
+
3149
+ # test replace the plot of pyTA of the polarity by this one --> should be quite easy TODO I think --> TODO
3150
+
3151
+ # ok --> it all seems to work --> see how I can handle that
3152
+
3153
+
3154
+
3155
+ # --> all seems ok now
3156
+ # --> put this in the advanced sql plotter
3157
+