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.
- batoolset/GUI/__init__.py +0 -0
- batoolset/GUI/customdialog.py +136 -0
- batoolset/GUI/exception_handler.py +74 -0
- batoolset/GUI/list_gui.py +514 -0
- batoolset/GUI/multi_image_display.py +1038 -0
- batoolset/GUI/open.py +325 -0
- batoolset/GUI/overlay_hints.py +45 -0
- batoolset/GUI/paint2.py +1369 -0
- batoolset/GUI/scrollablepaint.py +1165 -0
- batoolset/dialogs/__init__.py +0 -0
- batoolset/dialogs/opensave.py +228 -0
- batoolset/drawings/__init__.py +0 -0
- batoolset/drawings/math_text.py +294 -0
- batoolset/drawings/pdf_svg_renderer.py +376 -0
- batoolset/drawings/shapes/Position.py +183 -0
- batoolset/drawings/shapes/__init__.py +0 -0
- batoolset/drawings/shapes/automated_alignment_drawing_refactored.py +346 -0
- batoolset/drawings/shapes/circle2d.py +99 -0
- batoolset/drawings/shapes/compute_optimal_size_all_merged.py +328 -0
- batoolset/drawings/shapes/ellipse2d.py +336 -0
- batoolset/drawings/shapes/freehand2d.py +235 -0
- batoolset/drawings/shapes/group.py +1985 -0
- batoolset/drawings/shapes/image2d.py +3157 -0
- batoolset/drawings/shapes/line2d.py +579 -0
- batoolset/drawings/shapes/point2d.py +257 -0
- batoolset/drawings/shapes/polygon2d.py +357 -0
- batoolset/drawings/shapes/polyline2d.py +235 -0
- batoolset/drawings/shapes/rect2d.py +388 -0
- batoolset/drawings/shapes/rectangle2d.py +601 -0
- batoolset/drawings/shapes/scalebar.py +310 -0
- batoolset/drawings/shapes/serializablefont.py +133 -0
- batoolset/drawings/shapes/serializableguiparams.py +14 -0
- batoolset/drawings/shapes/square2d.py +102 -0
- batoolset/drawings/shapes/test_publication_style.py +220 -0
- batoolset/drawings/shapes/textorientation.py +45 -0
- batoolset/drawings/shapes/tools.py +467 -0
- batoolset/drawings/shapes/transform2D.py +350 -0
- batoolset/drawings/shapes/txt2d.py +615 -0
- batoolset/drawings/svg_clip_renderer.py +247 -0
- batoolset/drawings/svg_embed.py +165 -0
- batoolset/drawings/svg_text_renderer.py +183 -0
- batoolset/drawings/widgets/__init__.py +0 -0
- batoolset/drawings/widgets/col.py +343 -0
- batoolset/drawings/widgets/image_group.py +248 -0
- batoolset/drawings/widgets/image_organizer.py +96 -0
- batoolset/drawings/widgets/paint.py +184 -0
- batoolset/drawings/widgets/vectorial.py +256 -0
- batoolset/figure/__init__.py +0 -0
- batoolset/figure/alignment.py +822 -0
- batoolset/figure/column.py +1117 -0
- batoolset/figure/fig_tools.py +285 -0
- batoolset/figure/minimal_debugger_scale_etc.py +139 -0
- batoolset/figure/panel.py +697 -0
- batoolset/figure/row.py +852 -0
- batoolset/figure/user_selection.py +287 -0
- batoolset/icons/__init__.py +0 -0
- batoolset/icons/tools.py +248 -0
- batoolset/luts/__init__.py +0 -0
- batoolset/luts/lut_minimal_test.py +1308 -0
- batoolset/luts/lutcreatorfromformula.py +209 -0
- batoolset/luts/luteditor.py +243 -0
- batoolset/pyqts/__init__.py +0 -0
- batoolset/pyqts/pleasewaitdialog.py +14 -0
- batoolset/pyqts/tools.py +760 -0
- batoolset/serializations/__init__.py +0 -0
- batoolset/serializations/tools.py +1338 -0
- batoolset/theme.py +647 -0
- batoolset/uis/__init__.py +0 -0
- batoolset/uis/blinker.py +56 -0
- batoolset_gui-0.2.0.dist-info/METADATA +54 -0
- batoolset_gui-0.2.0.dist-info/RECORD +74 -0
- batoolset_gui-0.2.0.dist-info/WHEEL +5 -0
- batoolset_gui-0.2.0.dist-info/licenses/LICENSE +24 -0
- 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
|
+
|