pyvale 2025.5.3__cp311-cp311-win_amd64.whl → 2025.7.1__cp311-cp311-win_amd64.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.
Potentially problematic release.
This version of pyvale might be problematic. Click here for more details.
- pyvale/__init__.py +12 -0
- pyvale/blendercalibrationdata.py +3 -1
- pyvale/blenderscene.py +7 -5
- pyvale/blendertools.py +27 -5
- pyvale/camera.py +1 -0
- pyvale/cameradata.py +3 -0
- pyvale/camerasensor.py +147 -0
- pyvale/camerastereo.py +4 -4
- pyvale/cameratools.py +23 -61
- pyvale/cython/rastercyth.c +1657 -1352
- pyvale/cython/rastercyth.cp311-win_amd64.pyd +0 -0
- pyvale/cython/rastercyth.py +71 -26
- pyvale/data/DIC_Challenge_Star_Noise_Def.tiff +0 -0
- pyvale/data/DIC_Challenge_Star_Noise_Ref.tiff +0 -0
- pyvale/data/plate_hole_def0000.tiff +0 -0
- pyvale/data/plate_hole_def0001.tiff +0 -0
- pyvale/data/plate_hole_ref0000.tiff +0 -0
- pyvale/data/plate_rigid_def0000.tiff +0 -0
- pyvale/data/plate_rigid_def0001.tiff +0 -0
- pyvale/data/plate_rigid_ref0000.tiff +0 -0
- pyvale/dataset.py +96 -6
- pyvale/dic/cpp/dicbruteforce.cpp +370 -0
- pyvale/dic/cpp/dicfourier.cpp +648 -0
- pyvale/dic/cpp/dicinterpolator.cpp +559 -0
- pyvale/dic/cpp/dicmain.cpp +215 -0
- pyvale/dic/cpp/dicoptimizer.cpp +675 -0
- pyvale/dic/cpp/dicrg.cpp +137 -0
- pyvale/dic/cpp/dicscanmethod.cpp +677 -0
- pyvale/dic/cpp/dicsmooth.cpp +138 -0
- pyvale/dic/cpp/dicstrain.cpp +383 -0
- pyvale/dic/cpp/dicutil.cpp +563 -0
- pyvale/dic2d.py +164 -0
- pyvale/dic2dcpp.cp311-win_amd64.pyd +0 -0
- pyvale/dicchecks.py +476 -0
- pyvale/dicdataimport.py +247 -0
- pyvale/dicregionofinterest.py +887 -0
- pyvale/dicresults.py +55 -0
- pyvale/dicspecklegenerator.py +238 -0
- pyvale/dicspecklequality.py +305 -0
- pyvale/dicstrain.py +387 -0
- pyvale/dicstrainresults.py +37 -0
- pyvale/errorintegrator.py +10 -8
- pyvale/examples/basics/ex1_1_basicscalars_therm2d.py +124 -113
- pyvale/examples/basics/ex1_2_sensormodel_therm2d.py +124 -132
- pyvale/examples/basics/ex1_3_customsens_therm3d.py +199 -195
- pyvale/examples/basics/ex1_4_basicerrors_therm3d.py +125 -121
- pyvale/examples/basics/ex1_5_fielderrs_therm3d.py +145 -141
- pyvale/examples/basics/ex1_6_caliberrs_therm2d.py +96 -101
- pyvale/examples/basics/ex1_7_spatavg_therm2d.py +109 -105
- pyvale/examples/basics/ex2_1_basicvectors_disp2d.py +92 -91
- pyvale/examples/basics/ex2_2_vectorsens_disp2d.py +96 -90
- pyvale/examples/basics/ex2_3_sensangle_disp2d.py +88 -89
- pyvale/examples/basics/ex2_4_chainfielderrs_disp2d.py +172 -171
- pyvale/examples/basics/ex2_5_vectorfields3d_disp3d.py +88 -86
- pyvale/examples/basics/ex3_1_basictensors_strain2d.py +90 -90
- pyvale/examples/basics/ex3_2_tensorsens2d_strain2d.py +93 -91
- pyvale/examples/basics/ex3_3_tensorsens3d_strain3d.py +172 -160
- pyvale/examples/basics/ex4_1_expsim2d_thermmech2d.py +154 -148
- pyvale/examples/basics/ex4_2_expsim3d_thermmech3d.py +249 -231
- pyvale/examples/dic/ex1_region_of_interest.py +98 -0
- pyvale/examples/dic/ex2_plate_with_hole.py +149 -0
- pyvale/examples/dic/ex3_plate_with_hole_strain.py +93 -0
- pyvale/examples/dic/ex4_dic_blender.py +95 -0
- pyvale/examples/dic/ex5_dic_challenge.py +102 -0
- pyvale/examples/imagedef2d/ex_imagedef2d_todisk.py +4 -2
- pyvale/examples/renderblender/ex1_1_blenderscene.py +152 -105
- pyvale/examples/renderblender/ex1_2_blenderdeformed.py +151 -100
- pyvale/examples/renderblender/ex2_1_stereoscene.py +183 -116
- pyvale/examples/renderblender/ex2_2_stereodeformed.py +185 -112
- pyvale/examples/renderblender/ex3_1_blendercalibration.py +164 -109
- pyvale/examples/renderrasterisation/ex_rastenp.py +74 -35
- pyvale/examples/renderrasterisation/ex_rastercyth_oneframe.py +6 -13
- pyvale/examples/renderrasterisation/ex_rastercyth_static_cypara.py +2 -2
- pyvale/examples/renderrasterisation/ex_rastercyth_static_pypara.py +2 -4
- pyvale/imagedef2d.py +3 -2
- pyvale/imagetools.py +137 -0
- pyvale/rastercy.py +34 -4
- pyvale/rasternp.py +300 -276
- pyvale/rasteropts.py +58 -0
- pyvale/renderer.py +47 -0
- pyvale/rendermesh.py +52 -62
- pyvale/renderscene.py +51 -0
- pyvale/sensorarrayfactory.py +2 -2
- pyvale/sensortools.py +19 -35
- pyvale/simcases/case21.i +1 -1
- pyvale/simcases/run_1case.py +8 -0
- pyvale/simtools.py +2 -2
- pyvale/visualsimplotter.py +180 -0
- {pyvale-2025.5.3.dist-info → pyvale-2025.7.1.dist-info}/METADATA +11 -57
- {pyvale-2025.5.3.dist-info → pyvale-2025.7.1.dist-info}/RECORD +93 -56
- {pyvale-2025.5.3.dist-info → pyvale-2025.7.1.dist-info}/WHEEL +1 -1
- pyvale/examples/visualisation/ex1_1_plot_traces.py +0 -102
- pyvale/examples/visualisation/ex2_1_animate_sim.py +0 -89
- {pyvale-2025.5.3.dist-info → pyvale-2025.7.1.dist-info}/licenses/LICENSE +0 -0
- {pyvale-2025.5.3.dist-info → pyvale-2025.7.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
# ================================================================================
|
|
2
|
+
# pyvale: the python validation engine
|
|
3
|
+
# License: MIT
|
|
4
|
+
# Copyright (C) 2025 The Computer Aided Validation Team
|
|
5
|
+
# ================================================================================
|
|
6
|
+
|
|
7
|
+
from pyqtgraph.Qt import QtWidgets, QtCore
|
|
8
|
+
import pyqtgraph as pg
|
|
9
|
+
import cv2
|
|
10
|
+
import numpy as np
|
|
11
|
+
import matplotlib.pyplot as plt
|
|
12
|
+
from matplotlib.path import Path as mplPath
|
|
13
|
+
import math
|
|
14
|
+
import yaml
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
class DICRegionOfInterest:
|
|
19
|
+
"""
|
|
20
|
+
A class for interactively selecting and manipulating ROI of an image before passing to the DIC engine.
|
|
21
|
+
|
|
22
|
+
Users can:
|
|
23
|
+
- Interactively select rectangular, circular, or polygonal regions on the image.
|
|
24
|
+
- Add or subtract selected regions from the mask.
|
|
25
|
+
- Undo and reset the mask changes.
|
|
26
|
+
- read in previously created ROIs
|
|
27
|
+
- save created ROI as text or binary file.
|
|
28
|
+
- Display/save the ROI overlayed over the reference.
|
|
29
|
+
- Programatically select a rectangular ROI for consistency.
|
|
30
|
+
|
|
31
|
+
Public attributes:
|
|
32
|
+
image (np.ndarray): The image on which regions of interest are selected.
|
|
33
|
+
mask (np.ndarray): A binary mask representing the selected regions of interest.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, ref_image: str | np.ndarray | Path):
|
|
37
|
+
"""
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
ref_image : str, numpy.ndarray, pathlib.Path
|
|
41
|
+
location of the reference image.
|
|
42
|
+
|
|
43
|
+
Raises
|
|
44
|
+
------
|
|
45
|
+
ValueError: If the image cannot be loaded or is invalid.
|
|
46
|
+
"""
|
|
47
|
+
if isinstance(ref_image, str):
|
|
48
|
+
self.ref_image = cv2.imread(ref_image)
|
|
49
|
+
elif isinstance(ref_image, Path):
|
|
50
|
+
self.ref_image = cv2.imread(str(ref_image))
|
|
51
|
+
else:
|
|
52
|
+
self.ref_image = ref_image.copy()
|
|
53
|
+
|
|
54
|
+
if self.ref_image is None:
|
|
55
|
+
raise ValueError("Invalid image input")
|
|
56
|
+
|
|
57
|
+
self.mask = np.zeros(self.ref_image.shape[:2], dtype=bool)
|
|
58
|
+
self.seed = []
|
|
59
|
+
self.__roi_selected = False
|
|
60
|
+
self.roi_list = []
|
|
61
|
+
self.add_list = []
|
|
62
|
+
self.undo_list = []
|
|
63
|
+
|
|
64
|
+
# Drawing states
|
|
65
|
+
self.drawing_modes = {
|
|
66
|
+
'seed': False,
|
|
67
|
+
'rect': False,
|
|
68
|
+
'circle': False,
|
|
69
|
+
'poly': False
|
|
70
|
+
}
|
|
71
|
+
self.removing_modes = {
|
|
72
|
+
'rect': False,
|
|
73
|
+
'circle': False,
|
|
74
|
+
'poly': False
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# GUI elements (initialized in interactive_selection)
|
|
78
|
+
self.main_view = None
|
|
79
|
+
self.fill_layer = None
|
|
80
|
+
self.buttons = {}
|
|
81
|
+
self.poly_points = []
|
|
82
|
+
self.temp_mask = None
|
|
83
|
+
self.fill_array = None
|
|
84
|
+
self.height = None
|
|
85
|
+
self.width = None
|
|
86
|
+
self.subset_size = None
|
|
87
|
+
|
|
88
|
+
def interactive_selection(self, subset_size):
|
|
89
|
+
"""
|
|
90
|
+
Interactive GUI to select a region of interest (ROI) in the image using openCV.
|
|
91
|
+
"""
|
|
92
|
+
self.subset_size = subset_size
|
|
93
|
+
self.__roi_selected = True
|
|
94
|
+
|
|
95
|
+
# Initialize GUI
|
|
96
|
+
self._setup_gui()
|
|
97
|
+
self._setup_graphics()
|
|
98
|
+
self._connect_signals()
|
|
99
|
+
|
|
100
|
+
# Show and run
|
|
101
|
+
self.main_window.show()
|
|
102
|
+
pg.exec()
|
|
103
|
+
|
|
104
|
+
# Process final mask and seed
|
|
105
|
+
self._finalize_selection()
|
|
106
|
+
|
|
107
|
+
def _setup_gui(self):
|
|
108
|
+
"""Setup the main GUI window and sidebar."""
|
|
109
|
+
app = pg.mkQApp("ROI GUI")
|
|
110
|
+
self.main_window = CustomMainWindow(dic_obj=self)
|
|
111
|
+
main_layout = QtWidgets.QHBoxLayout()
|
|
112
|
+
self.main_window.setLayout(main_layout)
|
|
113
|
+
self.main_window.resize(1000, 1000)
|
|
114
|
+
|
|
115
|
+
# Create sidebar
|
|
116
|
+
sidebar = self._create_sidebar()
|
|
117
|
+
|
|
118
|
+
# Create graphics widget
|
|
119
|
+
self.graphics_widget = pg.GraphicsLayoutWidget()
|
|
120
|
+
|
|
121
|
+
main_layout.addLayout(sidebar)
|
|
122
|
+
main_layout.addWidget(self.graphics_widget)
|
|
123
|
+
|
|
124
|
+
def _create_sidebar(self):
|
|
125
|
+
"""Create the sidebar with all buttons."""
|
|
126
|
+
sidebar = QtWidgets.QVBoxLayout()
|
|
127
|
+
|
|
128
|
+
# Helper function for styled titles
|
|
129
|
+
def make_title(text):
|
|
130
|
+
label = QtWidgets.QLabel(text)
|
|
131
|
+
label.setStyleSheet("font-weight: bold; font-size: 14px; margin-top: 10px; margin-bottom: 5px;")
|
|
132
|
+
return label
|
|
133
|
+
|
|
134
|
+
# Create all buttons
|
|
135
|
+
button_configs = [
|
|
136
|
+
("FILE ACTIONS", [("open_roi", "Open ROI..."),
|
|
137
|
+
("save_roi", "Save Current ROI...")]),
|
|
138
|
+
|
|
139
|
+
("ADD SHAPES TO ROI", [("add_rect", "Add Rectangle"),
|
|
140
|
+
("add_circle", "Add Circle"),
|
|
141
|
+
("add_poly", "Add Polygon")]),
|
|
142
|
+
|
|
143
|
+
("REMOVE SHAPES FROM ROI", [("sub_rect", "Remove Rectangle"),
|
|
144
|
+
("sub_circle", "Remove Circle"),
|
|
145
|
+
("sub_poly", "Remove Polygon")]),
|
|
146
|
+
|
|
147
|
+
("SEED LOCATION", [("add_seed", "Add Reliability Guided Seed Location")]),
|
|
148
|
+
|
|
149
|
+
("UNDO / REDO SHAPES", [("undo_prev", "Undo Shape"),
|
|
150
|
+
("redo_prev", "Redo Shape")]),
|
|
151
|
+
|
|
152
|
+
("COMPLETION", [("finished", "ROI Completed")])
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
self.buttons = {}
|
|
156
|
+
for section_title, button_list in button_configs:
|
|
157
|
+
sidebar.addWidget(make_title(section_title))
|
|
158
|
+
for btn_id, btn_text in button_list:
|
|
159
|
+
btn = QtWidgets.QPushButton(btn_text)
|
|
160
|
+
self.buttons[btn_id] = btn
|
|
161
|
+
sidebar.addWidget(btn)
|
|
162
|
+
sidebar.addSpacing(20)
|
|
163
|
+
|
|
164
|
+
# Initial button states
|
|
165
|
+
self.buttons['undo_prev'].setEnabled(False)
|
|
166
|
+
self.buttons['redo_prev'].setEnabled(False)
|
|
167
|
+
|
|
168
|
+
sidebar.addStretch()
|
|
169
|
+
return sidebar
|
|
170
|
+
|
|
171
|
+
def _setup_graphics(self):
|
|
172
|
+
"""Setup the graphics view and image display."""
|
|
173
|
+
self.main_view = self.graphics_widget.addViewBox(lockAspect=True)
|
|
174
|
+
|
|
175
|
+
# Setup image
|
|
176
|
+
rotated = np.rot90(self.ref_image, k=-1)
|
|
177
|
+
img = pg.ImageItem(rotated)
|
|
178
|
+
self.main_view.addItem(img)
|
|
179
|
+
self.main_view.disableAutoRange('xy')
|
|
180
|
+
self.main_view.autoRange()
|
|
181
|
+
|
|
182
|
+
# Setup fill layer
|
|
183
|
+
self.fill_layer = pg.ImageItem()
|
|
184
|
+
self.fill_layer.setZValue(1)
|
|
185
|
+
self.main_view.addItem(self.fill_layer)
|
|
186
|
+
|
|
187
|
+
self.height, self.width = rotated.shape[:2]
|
|
188
|
+
self.fill_array = np.zeros((self.height, self.width, 4), dtype=np.uint8)
|
|
189
|
+
self.temp_mask = np.zeros((self.height, self.width), dtype=bool)
|
|
190
|
+
|
|
191
|
+
# Setup drawing overlays
|
|
192
|
+
self._setup_drawing_overlays()
|
|
193
|
+
|
|
194
|
+
def _setup_drawing_overlays(self):
|
|
195
|
+
"""Setup scatter plots and lines for polygon drawing."""
|
|
196
|
+
self.add_scatter = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush('b'))
|
|
197
|
+
self.sub_scatter = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush('r'))
|
|
198
|
+
self.add_line = pg.PlotDataItem(pen=pg.mkPen('b', width=3))
|
|
199
|
+
self.sub_line = pg.PlotDataItem(pen=pg.mkPen('r', width=3))
|
|
200
|
+
|
|
201
|
+
for item in [self.add_scatter, self.sub_scatter, self.add_line, self.sub_line]:
|
|
202
|
+
self.main_view.addItem(item)
|
|
203
|
+
|
|
204
|
+
def _connect_signals(self):
|
|
205
|
+
"""Connect all button signals to their handlers."""
|
|
206
|
+
signal_map = {
|
|
207
|
+
'add_seed': lambda: self._start_drawing_mode('seed'),
|
|
208
|
+
'add_rect': lambda: self._start_drawing_mode('rect'),
|
|
209
|
+
'add_circle': lambda: self._start_drawing_mode('circle'),
|
|
210
|
+
'add_poly': lambda: self._start_drawing_mode('poly'),
|
|
211
|
+
'sub_rect': lambda: self._start_removing_mode('rect'),
|
|
212
|
+
'sub_circle': lambda: self._start_removing_mode('circle'),
|
|
213
|
+
'sub_poly': lambda: self._start_removing_mode('poly'),
|
|
214
|
+
'undo_prev': self._undo_last,
|
|
215
|
+
'redo_prev': self._redo_last,
|
|
216
|
+
'save_roi': self._save_interactive_roi,
|
|
217
|
+
'open_roi': self._open_interactive_roi,
|
|
218
|
+
'finished': self._finish
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for btn_id, handler in signal_map.items():
|
|
222
|
+
self.buttons[btn_id].clicked.connect(handler)
|
|
223
|
+
|
|
224
|
+
self.main_view.scene().sigMouseClicked.connect(self._mouse_clicked)
|
|
225
|
+
|
|
226
|
+
def _start_drawing_mode(self, mode):
|
|
227
|
+
"""Start drawing mode for specified shape type."""
|
|
228
|
+
self._reset_all_modes()
|
|
229
|
+
self.drawing_modes[mode] = True
|
|
230
|
+
self.main_view.setCursor(QtCore.Qt.CursorShape.CrossCursor)
|
|
231
|
+
|
|
232
|
+
if mode == 'poly':
|
|
233
|
+
self.poly_points = []
|
|
234
|
+
self.add_scatter.setData([], [])
|
|
235
|
+
self.add_line.setData([], [])
|
|
236
|
+
print("Click to add polygon points. Right-click to finish.")
|
|
237
|
+
elif mode == 'seed':
|
|
238
|
+
self.buttons['add_seed'].setEnabled(False)
|
|
239
|
+
print("Click to add seed location...")
|
|
240
|
+
else:
|
|
241
|
+
print(f"Click to add {mode}.")
|
|
242
|
+
|
|
243
|
+
def _start_removing_mode(self, mode):
|
|
244
|
+
"""Start removing mode for specified shape type."""
|
|
245
|
+
self._reset_all_modes()
|
|
246
|
+
self.removing_modes[mode] = True
|
|
247
|
+
self.main_view.setCursor(QtCore.Qt.CursorShape.CrossCursor)
|
|
248
|
+
|
|
249
|
+
if mode == 'poly':
|
|
250
|
+
self.poly_points = []
|
|
251
|
+
self.sub_scatter.setData([], [])
|
|
252
|
+
self.sub_line.setData([], [])
|
|
253
|
+
print("Click to add polygon points. Right-click to finish.")
|
|
254
|
+
else:
|
|
255
|
+
print(f"Click to remove {mode}.")
|
|
256
|
+
|
|
257
|
+
def _reset_all_modes(self):
|
|
258
|
+
"""Reset all drawing and removing modes."""
|
|
259
|
+
for mode in self.drawing_modes:
|
|
260
|
+
self.drawing_modes[mode] = False
|
|
261
|
+
for mode in self.removing_modes:
|
|
262
|
+
self.removing_modes[mode] = False
|
|
263
|
+
|
|
264
|
+
def _finish_mode(self):
|
|
265
|
+
"""Finish current drawing/removing mode."""
|
|
266
|
+
self._reset_all_modes()
|
|
267
|
+
self.main_view.setCursor(QtCore.Qt.CursorShape.PointingHandCursor)
|
|
268
|
+
self._clear_redo_stack()
|
|
269
|
+
|
|
270
|
+
def _mouse_clicked(self, event):
|
|
271
|
+
"""Handle mouse clicks for drawing shapes."""
|
|
272
|
+
if self.drawing_modes['poly'] or self.removing_modes['poly']:
|
|
273
|
+
self._handle_polygon_click(event)
|
|
274
|
+
elif self.drawing_modes['seed']:
|
|
275
|
+
self._handle_seed_click(event)
|
|
276
|
+
elif any(self.drawing_modes.values()) or any(self.removing_modes.values()):
|
|
277
|
+
self._handle_shape_click(event)
|
|
278
|
+
|
|
279
|
+
def _handle_polygon_click(self, event):
|
|
280
|
+
"""Handle polygon drawing clicks."""
|
|
281
|
+
is_adding = self.drawing_modes['poly']
|
|
282
|
+
scatter = self.add_scatter if is_adding else self.sub_scatter
|
|
283
|
+
line = self.add_line if is_adding else self.sub_line
|
|
284
|
+
|
|
285
|
+
if event.button() == QtCore.Qt.MouseButton.LeftButton:
|
|
286
|
+
pos = event.scenePos()
|
|
287
|
+
if self.main_view.sceneBoundingRect().contains(pos):
|
|
288
|
+
mouse_point = self.main_view.mapSceneToView(pos)
|
|
289
|
+
self.poly_points.append([mouse_point.x(), mouse_point.y()])
|
|
290
|
+
scatter.setData([p[0] for p in self.poly_points], [p[1] for p in self.poly_points])
|
|
291
|
+
if len(self.poly_points) > 1:
|
|
292
|
+
line.setData([p[0] for p in self.poly_points], [p[1] for p in self.poly_points])
|
|
293
|
+
|
|
294
|
+
elif event.button() == QtCore.Qt.MouseButton.RightButton:
|
|
295
|
+
self._finish_polygon_drawing(is_adding)
|
|
296
|
+
|
|
297
|
+
def _handle_seed_click(self, event):
|
|
298
|
+
"""Handle seed location clicks."""
|
|
299
|
+
if event.button() == QtCore.Qt.MouseButton.LeftButton:
|
|
300
|
+
pos = event.scenePos()
|
|
301
|
+
start_point = self.main_view.mapSceneToView(pos)
|
|
302
|
+
|
|
303
|
+
x = math.floor(start_point.x() - self.subset_size / 2)
|
|
304
|
+
y = math.floor(start_point.y() - self.subset_size / 2)
|
|
305
|
+
|
|
306
|
+
seed_roi = pg.RectROI(
|
|
307
|
+
[x, y], [self.subset_size, self.subset_size],
|
|
308
|
+
pen=pg.mkPen('b', width=3),
|
|
309
|
+
hoverPen=pg.mkPen('y', width=3),
|
|
310
|
+
handlePen='#0000',
|
|
311
|
+
handleHoverPen='#0000'
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Remove all handles to make it non-interactive
|
|
315
|
+
for handle in seed_roi.getHandles():
|
|
316
|
+
seed_roi.removeHandle(handle)
|
|
317
|
+
|
|
318
|
+
self.main_view.addItem(seed_roi)
|
|
319
|
+
self.seed_roi = seed_roi
|
|
320
|
+
print(f"Seed initially added at location: [{x}, {self.width-y}]")
|
|
321
|
+
self._finish_mode()
|
|
322
|
+
|
|
323
|
+
def _handle_shape_click(self, event):
|
|
324
|
+
"""Handle rectangle and circle drawing clicks."""
|
|
325
|
+
if event.button() != QtCore.Qt.MouseButton.LeftButton:
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
pos = event.scenePos()
|
|
329
|
+
start_point = self.main_view.mapSceneToView(pos)
|
|
330
|
+
|
|
331
|
+
# Determine shape type and add/remove mode
|
|
332
|
+
shape_type = None
|
|
333
|
+
is_adding = True
|
|
334
|
+
|
|
335
|
+
for mode in ['rect', 'circle']:
|
|
336
|
+
if self.drawing_modes[mode]:
|
|
337
|
+
shape_type = mode
|
|
338
|
+
is_adding = True
|
|
339
|
+
break
|
|
340
|
+
elif self.removing_modes[mode]:
|
|
341
|
+
shape_type = mode
|
|
342
|
+
is_adding = False
|
|
343
|
+
break
|
|
344
|
+
|
|
345
|
+
if shape_type:
|
|
346
|
+
roi = self._create_shape_roi(shape_type, start_point, is_adding)
|
|
347
|
+
self._add_roi_to_scene(roi, is_adding)
|
|
348
|
+
self._finish_mode()
|
|
349
|
+
|
|
350
|
+
def _create_shape_roi(self, shape_type, start_point, is_adding):
|
|
351
|
+
"""Create ROI object for rectangle or circle."""
|
|
352
|
+
pen = pg.mkPen('g', width=4) if is_adding else pg.mkPen('r', width=4)
|
|
353
|
+
hover_pen = pg.mkPen('b', width=4)
|
|
354
|
+
handle_pen = pg.mkPen('b', width=4)
|
|
355
|
+
|
|
356
|
+
if shape_type == 'rect':
|
|
357
|
+
roi = pg.RectROI(
|
|
358
|
+
start_point, [self.height/6, self.width/6],
|
|
359
|
+
pen=pen, hoverPen=hover_pen, handlePen=handle_pen, handleHoverPen=hover_pen
|
|
360
|
+
)
|
|
361
|
+
roi.addScaleHandle([1, 0], [0.0, 1.0])
|
|
362
|
+
roi.addScaleHandle([0, 1], [1.0, 0.0])
|
|
363
|
+
roi.addScaleHandle([0, 0], [1.0, 1.0])
|
|
364
|
+
roi.addTranslateHandle([0.5, 0.5])
|
|
365
|
+
|
|
366
|
+
elif shape_type == 'circle':
|
|
367
|
+
x = start_point.x() - self.width / 10
|
|
368
|
+
y = start_point.y() - self.width / 10
|
|
369
|
+
roi = pg.CircleROI(
|
|
370
|
+
[x, y], radius=self.width/10,
|
|
371
|
+
pen=pen, hoverPen=hover_pen, handlePen=handle_pen, handleHoverPen=hover_pen
|
|
372
|
+
)
|
|
373
|
+
roi.addTranslateHandle([0.5, 0.5])
|
|
374
|
+
|
|
375
|
+
# Style handles
|
|
376
|
+
for handle in roi.getHandles():
|
|
377
|
+
handle.radius = 10
|
|
378
|
+
handle.buildPath()
|
|
379
|
+
handle.update()
|
|
380
|
+
|
|
381
|
+
return roi
|
|
382
|
+
|
|
383
|
+
def _add_roi_to_scene(self, roi, is_adding):
|
|
384
|
+
"""Add ROI to scene and lists."""
|
|
385
|
+
self.roi_list.append(roi)
|
|
386
|
+
self.add_list.append(is_adding)
|
|
387
|
+
self.main_view.addItem(roi)
|
|
388
|
+
roi.sigRegionChanged.connect(self._redraw_fill_layer)
|
|
389
|
+
self._redraw_fill_layer()
|
|
390
|
+
self._update_button_states()
|
|
391
|
+
|
|
392
|
+
def _finish_polygon_drawing(self, is_adding):
|
|
393
|
+
"""Finish polygon drawing."""
|
|
394
|
+
if len(self.poly_points) >= 3:
|
|
395
|
+
pen = pg.mkPen('g', width=4) if is_adding else pg.mkPen('r', width=4)
|
|
396
|
+
hover_pen = pg.mkPen('b', width=4)
|
|
397
|
+
handle_pen = pg.mkPen('b', width=4)
|
|
398
|
+
|
|
399
|
+
roi = pg.PolyLineROI(
|
|
400
|
+
self.poly_points, closed=True,
|
|
401
|
+
pen=pen, hoverPen=hover_pen, handlePen=handle_pen, handleHoverPen=hover_pen
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
for handle in roi.getHandles():
|
|
405
|
+
handle.radius = 10
|
|
406
|
+
handle.buildPath()
|
|
407
|
+
handle.update()
|
|
408
|
+
|
|
409
|
+
self._add_roi_to_scene(roi, is_adding)
|
|
410
|
+
print("Polygon added.")
|
|
411
|
+
else:
|
|
412
|
+
print("Need at least 3 points.")
|
|
413
|
+
|
|
414
|
+
# Clean up
|
|
415
|
+
self.poly_points = []
|
|
416
|
+
scatter = self.add_scatter if is_adding else self.sub_scatter
|
|
417
|
+
line = self.add_line if is_adding else self.sub_line
|
|
418
|
+
scatter.setData([], [])
|
|
419
|
+
line.setData([], [])
|
|
420
|
+
self._finish_mode()
|
|
421
|
+
|
|
422
|
+
def _redraw_fill_layer(self):
|
|
423
|
+
"""Redraw the fill layer based on current ROIs."""
|
|
424
|
+
if not self.roi_list:
|
|
425
|
+
self.fill_array.fill(0)
|
|
426
|
+
self.temp_mask.fill(False)
|
|
427
|
+
self.fill_layer.setImage(self.fill_array)
|
|
428
|
+
return
|
|
429
|
+
|
|
430
|
+
self.temp_mask.fill(False)
|
|
431
|
+
|
|
432
|
+
for roi, is_adding in zip(self.roi_list, self.add_list):
|
|
433
|
+
if isinstance(roi, pg.RectROI):
|
|
434
|
+
self._apply_rect_mask(roi, is_adding)
|
|
435
|
+
elif isinstance(roi, pg.CircleROI):
|
|
436
|
+
self._apply_circle_mask(roi, is_adding)
|
|
437
|
+
elif isinstance(roi, pg.PolyLineROI):
|
|
438
|
+
self._apply_poly_mask(roi, is_adding)
|
|
439
|
+
|
|
440
|
+
# Update fill array
|
|
441
|
+
self.fill_array[:, :, 0] = 0
|
|
442
|
+
self.fill_array[:, :, 1] = 255
|
|
443
|
+
self.fill_array[:, :, 2] = 0
|
|
444
|
+
self.fill_array[:, :, 3] = self.temp_mask * 80
|
|
445
|
+
self.fill_layer.setImage(self.fill_array)
|
|
446
|
+
|
|
447
|
+
def _apply_rect_mask(self, roi, is_adding):
|
|
448
|
+
"""Apply rectangle mask to temp_mask."""
|
|
449
|
+
pos = roi.pos()
|
|
450
|
+
size = roi.size()
|
|
451
|
+
x, y = int(pos[1]), int(pos[0])
|
|
452
|
+
w, h = int(size[1]), int(size[0])
|
|
453
|
+
|
|
454
|
+
# Clamp to image bounds
|
|
455
|
+
x = max(0, min(x, self.width))
|
|
456
|
+
y = max(0, min(y, self.height))
|
|
457
|
+
w = max(0, min(w, self.width - x))
|
|
458
|
+
h = max(0, min(h, self.height - y))
|
|
459
|
+
|
|
460
|
+
if w > 0 and h > 0:
|
|
461
|
+
self.temp_mask[y:y+h, x:x+w] = is_adding
|
|
462
|
+
|
|
463
|
+
def _apply_circle_mask(self, roi, is_adding):
|
|
464
|
+
"""Apply circle mask to temp_mask."""
|
|
465
|
+
pos = roi.pos()
|
|
466
|
+
size = roi.size()
|
|
467
|
+
cx, cy = pos[1] + size[1]/2, pos[0] + size[0]/2
|
|
468
|
+
rx, ry = size[1]/2, size[0]/2
|
|
469
|
+
|
|
470
|
+
y_coords, x_coords = np.ogrid[:self.height, :self.width]
|
|
471
|
+
circle_mask = ((x_coords - cx)/rx)**2 + ((y_coords - cy)/ry)**2 <= 1
|
|
472
|
+
|
|
473
|
+
if is_adding:
|
|
474
|
+
self.temp_mask |= circle_mask
|
|
475
|
+
else:
|
|
476
|
+
self.temp_mask &= ~circle_mask
|
|
477
|
+
|
|
478
|
+
def _apply_poly_mask(self, roi, is_adding):
|
|
479
|
+
"""Apply polygon mask to temp_mask."""
|
|
480
|
+
points = roi.getState()['points']
|
|
481
|
+
pos = roi.pos()
|
|
482
|
+
|
|
483
|
+
if len(points) >= 3:
|
|
484
|
+
vertices = np.array([(p[1]+pos[1], p[0]+pos[0]) for p in points])
|
|
485
|
+
path = mplPath(vertices)
|
|
486
|
+
|
|
487
|
+
x_min, x_max = int(np.floor(vertices[:, 0].min())), int(np.ceil(vertices[:, 0].max()))
|
|
488
|
+
y_min, y_max = int(np.floor(vertices[:, 1].min())), int(np.ceil(vertices[:, 1].max()))
|
|
489
|
+
|
|
490
|
+
# Clamp to image bounds
|
|
491
|
+
x_min = max(0, x_min)
|
|
492
|
+
x_max = min(self.width, x_max)
|
|
493
|
+
y_min = max(0, y_min)
|
|
494
|
+
y_max = min(self.height, y_max)
|
|
495
|
+
|
|
496
|
+
if x_max > x_min and y_max > y_min:
|
|
497
|
+
xx, yy = np.meshgrid(np.arange(x_min, x_max), np.arange(y_min, y_max))
|
|
498
|
+
points_grid = np.column_stack((xx.ravel(), yy.ravel()))
|
|
499
|
+
inside = path.contains_points(points_grid)
|
|
500
|
+
inside_2d = inside.reshape(y_max - y_min, x_max - x_min)
|
|
501
|
+
|
|
502
|
+
if is_adding:
|
|
503
|
+
self.temp_mask[y_min:y_max, x_min:x_max] |= inside_2d
|
|
504
|
+
else:
|
|
505
|
+
self.temp_mask[y_min:y_max, x_min:x_max] &= ~inside_2d
|
|
506
|
+
|
|
507
|
+
def _update_button_states(self):
|
|
508
|
+
"""Update the enabled state of undo and redo buttons."""
|
|
509
|
+
self.buttons['undo_prev'].setEnabled(len(self.roi_list) > 0)
|
|
510
|
+
self.buttons['redo_prev'].setEnabled(len(self.undo_list) > 0)
|
|
511
|
+
|
|
512
|
+
def _clear_redo_stack(self):
|
|
513
|
+
"""Clear the redo stack when new shapes are added."""
|
|
514
|
+
self.undo_list = []
|
|
515
|
+
self._update_button_states()
|
|
516
|
+
|
|
517
|
+
def _undo_last(self):
|
|
518
|
+
"""Undo the last ROI operation."""
|
|
519
|
+
if self.roi_list:
|
|
520
|
+
roi = self.roi_list.pop()
|
|
521
|
+
add_flag = self.add_list.pop()
|
|
522
|
+
self.main_view.removeItem(roi)
|
|
523
|
+
self.undo_list.append((roi, add_flag))
|
|
524
|
+
self._redraw_fill_layer()
|
|
525
|
+
self._update_button_states()
|
|
526
|
+
|
|
527
|
+
def _redo_last(self):
|
|
528
|
+
"""Redo the last undone ROI operation."""
|
|
529
|
+
if self.undo_list:
|
|
530
|
+
roi, add_flag = self.undo_list.pop()
|
|
531
|
+
self.roi_list.append(roi)
|
|
532
|
+
self.add_list.append(add_flag)
|
|
533
|
+
self.main_view.addItem(roi)
|
|
534
|
+
roi.sigRegionChanged.connect(self._redraw_fill_layer)
|
|
535
|
+
self._redraw_fill_layer()
|
|
536
|
+
self._update_button_states()
|
|
537
|
+
|
|
538
|
+
def _save_interactive_roi(self):
|
|
539
|
+
"""Save the current ROI to a YAML file."""
|
|
540
|
+
filename, _ = QtWidgets.QFileDialog.getSaveFileName(self.main_window, 'Save ROI', 'roi_interactive.yaml', filter='YAML Files (*.yaml)')
|
|
541
|
+
|
|
542
|
+
if filename:
|
|
543
|
+
|
|
544
|
+
# Ensure extension is added if user doesn't include it
|
|
545
|
+
if filename and not filename.endswith('.yaml'):
|
|
546
|
+
filename += '.yaml'
|
|
547
|
+
|
|
548
|
+
print("Saving to file:", filename)
|
|
549
|
+
serialized = [
|
|
550
|
+
self._get_roi_data(roi, add)
|
|
551
|
+
for roi, add in zip(self.roi_list, self.add_list)
|
|
552
|
+
]
|
|
553
|
+
|
|
554
|
+
# add ROI to serialized data
|
|
555
|
+
if hasattr(self, 'seed_roi'):
|
|
556
|
+
self._finalize_selection()
|
|
557
|
+
seed_data = {
|
|
558
|
+
'type': 'SeedROI',
|
|
559
|
+
'pos': [self.seed[0], self.seed[1]],
|
|
560
|
+
'size': [self.subset_size, self.subset_size],
|
|
561
|
+
'add': True
|
|
562
|
+
}
|
|
563
|
+
serialized.append(seed_data)
|
|
564
|
+
|
|
565
|
+
with open(filename, 'w') as f:
|
|
566
|
+
yaml.dump(serialized, f, sort_keys=False)
|
|
567
|
+
|
|
568
|
+
def _open_interactive_roi(self):
|
|
569
|
+
"""Open ROI from a YAML file."""
|
|
570
|
+
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
|
|
571
|
+
self.main_window, 'Open ROI', filter='YAML Files (*.yaml)'
|
|
572
|
+
)
|
|
573
|
+
if filename:
|
|
574
|
+
with open(filename, 'r') as f:
|
|
575
|
+
data = yaml.safe_load(f)
|
|
576
|
+
|
|
577
|
+
# Clear existing ROIs
|
|
578
|
+
for roi in self.roi_list:
|
|
579
|
+
self.main_view.removeItem(roi)
|
|
580
|
+
self.roi_list = []
|
|
581
|
+
self.add_list = []
|
|
582
|
+
|
|
583
|
+
self.seed_roi = None # Clear existing seed
|
|
584
|
+
|
|
585
|
+
for entry in data:
|
|
586
|
+
if entry.get('type') == 'SeedROI':
|
|
587
|
+
# Restore the seed ROI
|
|
588
|
+
x, y = entry['pos']
|
|
589
|
+
size = entry.get('size', [10, 10]) # fallback default
|
|
590
|
+
self.seed_roi = pg.RectROI(
|
|
591
|
+
[x, y], size,
|
|
592
|
+
pen=pg.mkPen('b', width=3),
|
|
593
|
+
hoverPen=pg.mkPen('y', width=3),
|
|
594
|
+
handlePen='#0000',
|
|
595
|
+
handleHoverPen='#0000'
|
|
596
|
+
)
|
|
597
|
+
self.main_view.addItem(self.seed_roi)
|
|
598
|
+
|
|
599
|
+
else:
|
|
600
|
+
# Restore standard ROI
|
|
601
|
+
roi = self._create_roi_from_data(entry)
|
|
602
|
+
self.roi_list.append(roi)
|
|
603
|
+
self.add_list.append(entry['add'])
|
|
604
|
+
self.main_view.addItem(roi)
|
|
605
|
+
roi.sigRegionChanged.connect(self._redraw_fill_layer)
|
|
606
|
+
|
|
607
|
+
self._redraw_fill_layer()
|
|
608
|
+
self._update_button_states()
|
|
609
|
+
|
|
610
|
+
def _create_roi_from_data(self, entry):
|
|
611
|
+
"""Create ROI object from saved data."""
|
|
612
|
+
roi_type = entry['type']
|
|
613
|
+
is_adding = entry['add']
|
|
614
|
+
|
|
615
|
+
pen = pg.mkPen('g', width=4) if is_adding else pg.mkPen('r', width=4)
|
|
616
|
+
hover_pen = pg.mkPen('b', width=4)
|
|
617
|
+
handle_pen = pg.mkPen('b', width=4)
|
|
618
|
+
|
|
619
|
+
if roi_type == 'RectROI':
|
|
620
|
+
roi = pg.RectROI(entry['pos'], entry['size'], pen=pen,
|
|
621
|
+
hoverPen=hover_pen, handlePen=handle_pen,
|
|
622
|
+
handleHoverPen=hover_pen)
|
|
623
|
+
|
|
624
|
+
roi.addScaleHandle([1, 0], [0.0, 1.0])
|
|
625
|
+
roi.addScaleHandle([0, 1], [1.0, 0.0])
|
|
626
|
+
roi.addScaleHandle([0, 0], [1.0, 1.0])
|
|
627
|
+
roi.addTranslateHandle([0.5, 0.5])
|
|
628
|
+
|
|
629
|
+
elif roi_type == 'CircleROI':
|
|
630
|
+
roi = pg.CircleROI(entry['pos'], entry['size'], pen=pen,
|
|
631
|
+
hoverPen=hover_pen, handlePen=handle_pen,
|
|
632
|
+
handleHoverPen=hover_pen)
|
|
633
|
+
roi.addTranslateHandle([0.5, 0.5])
|
|
634
|
+
|
|
635
|
+
elif roi_type == 'PolyLineROI':
|
|
636
|
+
points = [QtCore.QPointF(p[0], p[1]) for p in entry['points']]
|
|
637
|
+
roi = pg.PolyLineROI(points, closed=True,pen=pen,
|
|
638
|
+
hoverPen=hover_pen, handlePen=handle_pen,
|
|
639
|
+
handleHoverPen=hover_pen)
|
|
640
|
+
|
|
641
|
+
else:
|
|
642
|
+
raise TypeError(f"Unsupported ROI type: {roi_type}")
|
|
643
|
+
|
|
644
|
+
#update handle sizes
|
|
645
|
+
for handle in roi.getHandles():
|
|
646
|
+
handle.radius = 10
|
|
647
|
+
handle.buildPath()
|
|
648
|
+
handle.update()
|
|
649
|
+
|
|
650
|
+
return roi
|
|
651
|
+
|
|
652
|
+
def _finish(self):
|
|
653
|
+
"""Finish ROI selection and close the GUI, with a check for empty seed."""
|
|
654
|
+
|
|
655
|
+
self._finalize_selection()
|
|
656
|
+
|
|
657
|
+
if not self.seed:
|
|
658
|
+
reply = QtWidgets.QMessageBox.question(
|
|
659
|
+
self.main_window,
|
|
660
|
+
"Exit Confirmation",
|
|
661
|
+
"No Seed location has been selected for reliability guided DIC. Are you sure you want to continue?",
|
|
662
|
+
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
|
|
663
|
+
QtWidgets.QMessageBox.StandardButton.No
|
|
664
|
+
)
|
|
665
|
+
if reply == QtWidgets.QMessageBox.StandardButton.No:
|
|
666
|
+
return
|
|
667
|
+
|
|
668
|
+
self.main_window.close()
|
|
669
|
+
pg.QtWidgets.QApplication.quit()
|
|
670
|
+
|
|
671
|
+
def _finalize_selection(self):
|
|
672
|
+
"""Process the final mask and seed location."""
|
|
673
|
+
self.mask = np.flipud(self.temp_mask.T)
|
|
674
|
+
|
|
675
|
+
if hasattr(self, 'seed_roi'):
|
|
676
|
+
pos = self.seed_roi.pos()
|
|
677
|
+
x = int(np.floor(pos.x()))
|
|
678
|
+
y = int(np.floor(self.width - pos.y()))
|
|
679
|
+
self.seed = [x, y]
|
|
680
|
+
|
|
681
|
+
if not self.mask[y, x]:
|
|
682
|
+
raise ValueError(f"Seed location [{x}, {y}] is not within the mask")
|
|
683
|
+
print(f"Final seed location: [{x}, {y}]")
|
|
684
|
+
|
|
685
|
+
def _get_roi_data(self, roi_element, add: bool):
|
|
686
|
+
"""Extract data from ROI element for serialization."""
|
|
687
|
+
if isinstance(roi_element, pg.RectROI):
|
|
688
|
+
return {
|
|
689
|
+
'type': 'RectROI',
|
|
690
|
+
'pos': [float(roi_element.pos().x()), float(roi_element.pos().y())],
|
|
691
|
+
'size': [float(roi_element.size().x()), float(roi_element.size().y())],
|
|
692
|
+
'add': bool(add)
|
|
693
|
+
}
|
|
694
|
+
elif isinstance(roi_element, pg.CircleROI):
|
|
695
|
+
return {
|
|
696
|
+
'type': 'CircleROI',
|
|
697
|
+
'pos': [float(roi_element.pos().x()), float(roi_element.pos().y())],
|
|
698
|
+
'size': [float(roi_element.size().x()), float(roi_element.size().y())],
|
|
699
|
+
'add': bool(add)
|
|
700
|
+
}
|
|
701
|
+
elif isinstance(roi_element, pg.PolyLineROI):
|
|
702
|
+
handle_pos = roi_element.getLocalHandlePositions()
|
|
703
|
+
points = [[float(p[1].x()), float(p[1].y())] for p in handle_pos]
|
|
704
|
+
return {
|
|
705
|
+
'type': 'PolyLineROI',
|
|
706
|
+
'points': points,
|
|
707
|
+
'add': bool(add)
|
|
708
|
+
}
|
|
709
|
+
else:
|
|
710
|
+
raise TypeError(f"Unsupported ROI type: {type(roi_element)}")
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def reset_mask(self):
|
|
714
|
+
"""
|
|
715
|
+
Completely resets the roi mask to 0s.
|
|
716
|
+
"""
|
|
717
|
+
self.mask[:] = False;
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def rect_boundary(self, left: int, right: int, top: int, bottom: int) -> None:
|
|
722
|
+
"""
|
|
723
|
+
Defines a central rectangular region of interest (ROI) excluding
|
|
724
|
+
surrounding pixels defined by input arguments"
|
|
725
|
+
|
|
726
|
+
Parameters
|
|
727
|
+
----------
|
|
728
|
+
left (int): Number of px to exclude from left edge.
|
|
729
|
+
right (int): Number of px to exclude from the right edge.
|
|
730
|
+
top (int): Number of px to exclude from the top edge.
|
|
731
|
+
bottom (int): Number of px to exclude from the bottom edge.
|
|
732
|
+
"""
|
|
733
|
+
self.reset_mask()
|
|
734
|
+
self.mask[bottom:(self.ref_image.shape[0]-top), left:(self.ref_image.shape[1])-right] = 255
|
|
735
|
+
self.__roi_selected = True
|
|
736
|
+
|
|
737
|
+
def rect_region(self, x: int, y: int, size_x: int, size_y: int ) -> None:
|
|
738
|
+
|
|
739
|
+
top = max(0, y)
|
|
740
|
+
bottom = min(self.ref_image.shape[0],y+size_y)
|
|
741
|
+
left = max(0, x)
|
|
742
|
+
right = min(self.ref_image.shape[1],x+size_x)
|
|
743
|
+
|
|
744
|
+
# Apply the mask in the subset region
|
|
745
|
+
self.mask[top:bottom, left:right] = 255
|
|
746
|
+
self.__roi_selected = True
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def save_image(self, filename: str | Path) -> None:
|
|
752
|
+
"""
|
|
753
|
+
Save the ROI overlayed over the reference image in .tiff image format.
|
|
754
|
+
|
|
755
|
+
Parameters
|
|
756
|
+
----------
|
|
757
|
+
filename : str or pathlib.Path
|
|
758
|
+
Filename of image
|
|
759
|
+
|
|
760
|
+
Raises
|
|
761
|
+
------
|
|
762
|
+
ValueError
|
|
763
|
+
If no ROI has been selected
|
|
764
|
+
"""
|
|
765
|
+
if not self.__roi_selected:
|
|
766
|
+
raise ValueError("No ROI selected with \'interactive_selection\', \'rect_boundary\', \'read_array\' or \'rect_region\'. ")
|
|
767
|
+
|
|
768
|
+
overlay = self.ref_image.copy()
|
|
769
|
+
overlay[self.mask] = (0, 255, 0)
|
|
770
|
+
result = cv2.addWeighted(self.ref_image, 0.6, overlay, 0.4, 0)
|
|
771
|
+
cv2.imwrite(str(filename), result)
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def save_array(self, filename: str | Path, binary: bool=False) -> None:
|
|
777
|
+
"""
|
|
778
|
+
Save the ROI mask as a numpy binary or text file.
|
|
779
|
+
|
|
780
|
+
Parameters
|
|
781
|
+
----------
|
|
782
|
+
filename : str or pathlib.Path
|
|
783
|
+
filename given to saved ROI mask
|
|
784
|
+
binary : bool
|
|
785
|
+
If True, saves from as a .npy binary file.
|
|
786
|
+
If False, saves to a space delimited text file.
|
|
787
|
+
|
|
788
|
+
Raises
|
|
789
|
+
------
|
|
790
|
+
ValueError
|
|
791
|
+
If no ROI has been selected.
|
|
792
|
+
"""
|
|
793
|
+
if not self.__roi_selected:
|
|
794
|
+
raise ValueError("No ROI selected with \'interactive_selection\', \'rect_boundary\', \'read_array\' or \'rect_region\'. ")
|
|
795
|
+
|
|
796
|
+
if binary:
|
|
797
|
+
np.save(filename, self.mask)
|
|
798
|
+
else:
|
|
799
|
+
np.savetxt(filename, self.mask, fmt='%d', delimiter=' ')
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def read_array(self, filename: str | Path, binary: bool = False) -> None:
|
|
803
|
+
"""
|
|
804
|
+
Load the ROI mask from a binary or text file and store it in `self.mask`.
|
|
805
|
+
|
|
806
|
+
Parameters
|
|
807
|
+
----------
|
|
808
|
+
filename : str or pathlib.Path
|
|
809
|
+
Path to the file to load.
|
|
810
|
+
binary : bool
|
|
811
|
+
If True, loads from a .npy binary file. If False, loads from a text file.
|
|
812
|
+
|
|
813
|
+
Raises
|
|
814
|
+
------
|
|
815
|
+
FileNotFoundError
|
|
816
|
+
If the specified file does not exist.
|
|
817
|
+
ValueError
|
|
818
|
+
If the loaded data is not a valid mask.
|
|
819
|
+
"""
|
|
820
|
+
|
|
821
|
+
if not os.path.exists(filename):
|
|
822
|
+
raise FileNotFoundError(f"File '{filename}' does not exist.")
|
|
823
|
+
|
|
824
|
+
if binary:
|
|
825
|
+
self.mask = np.load(filename)
|
|
826
|
+
else:
|
|
827
|
+
self.mask = np.loadtxt(filename, dtype=bool, delimiter=' ')
|
|
828
|
+
|
|
829
|
+
# Optional: check if the loaded data is a proper binary mask (0s and 1s)
|
|
830
|
+
if not np.isin(self.mask, [0, 1]).all():
|
|
831
|
+
raise ValueError("Loaded ROI mask contains values other than 0 and 1.")
|
|
832
|
+
|
|
833
|
+
self.__roi_selected = True
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def show_image(self) -> None:
|
|
837
|
+
"""
|
|
838
|
+
Displays the current mask in grayscale.
|
|
839
|
+
|
|
840
|
+
Raises
|
|
841
|
+
------
|
|
842
|
+
ValueError: If no ROI is selected.
|
|
843
|
+
"""
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
if not self.__roi_selected:
|
|
847
|
+
raise ValueError("No ROI selected with 'interactive_selection' or 'rect_boundary'")
|
|
848
|
+
|
|
849
|
+
# Create a green mask image
|
|
850
|
+
green_mask = np.zeros_like(self.ref_image)
|
|
851
|
+
|
|
852
|
+
green_mask[self.mask,:] = [0, 255, 0]
|
|
853
|
+
|
|
854
|
+
# Blend the original image and the mask
|
|
855
|
+
blended = self.ref_image.astype(float) * 0.7 + green_mask.astype(float) * 0.3
|
|
856
|
+
blended = blended.astype(np.uint8)
|
|
857
|
+
|
|
858
|
+
# Display using Matplotlib
|
|
859
|
+
plt.figure()
|
|
860
|
+
plt.imshow(blended)
|
|
861
|
+
plt.axis('off')
|
|
862
|
+
plt.tight_layout()
|
|
863
|
+
plt.show()
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
class CustomMainWindow(QtWidgets.QWidget):
|
|
867
|
+
def __init__(self, dic_obj=None, *args, **kwargs):
|
|
868
|
+
super().__init__(*args, **kwargs)
|
|
869
|
+
self.dic_obj = dic_obj
|
|
870
|
+
|
|
871
|
+
def closeEvent(self, event):
|
|
872
|
+
if self.dic_obj:
|
|
873
|
+
# Force finalization before checking seed
|
|
874
|
+
self.dic_obj._finalize_selection()
|
|
875
|
+
|
|
876
|
+
if not self.dic_obj.seed:
|
|
877
|
+
reply = QtWidgets.QMessageBox.question(
|
|
878
|
+
self,
|
|
879
|
+
"Exit Confirmation",
|
|
880
|
+
"No Seed location has been selected for reliability guided DIC. Are you sure you want to continue?",
|
|
881
|
+
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
|
|
882
|
+
QtWidgets.QMessageBox.StandardButton.No
|
|
883
|
+
)
|
|
884
|
+
if reply == QtWidgets.QMessageBox.StandardButton.No:
|
|
885
|
+
event.ignore()
|
|
886
|
+
return
|
|
887
|
+
event.accept()
|