-
Notifications
You must be signed in to change notification settings - Fork 2
/
main.py
385 lines (304 loc) · 14.6 KB
/
main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
from PyQt6 import QtWidgets
from PyQt6.QtWidgets import QVBoxLayout, QMessageBox
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIcon
from PyQt6 import QtWidgets, uic
import numpy as np
import sys
import qdarkstyle
from PyQt6.QtCore import Qt, QRect
import sys
import logging
from imageViewPort import ImageViewport
from FTViewPort import FTViewPort
from OutViewPort import OutViewPort
from mixer import ImageMixer
from ThreadingClass import WorkerSignals, WorkerThread
# Configure logging to capture all log levels
logging.basicConfig(filemode="a", filename="our_log.log",
format="(%(asctime)s) | %(name)s| %(levelname)s | => %(message)s", level=logging.INFO)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.init_ui()
def init_ui(self):
"""
Initializes the user interface by loading the UI page, setting the window title and icon,
initializing various attributes, connecting signals to slots, loading UI elements, and
setting the window to full screen.
Parameters:
None
Returns:
None
"""
# Load the UI Page
self.ui = uic.loadUi('Mainwindow.ui', self)
self.setWindowTitle("Image Mixer")
self.setWindowIcon(QIcon("icons/mixer.png"))
self.image_ports = []
self.components_ports = []
self.images_areas = np.repeat(np.inf, 4) # initialized
self.out_ports = []
self.open_order = []
self.min_width = None
self.min_height = None
self.curr_mode = None
self.worker_thread = None
self.components = {"1": '', "2": '', '3': '', '4': ''}
self.ui.output1_port.resize(
self.ui.original1.width(), self.ui.original1.height())
# mixer and its connection line
self.mixer = ImageMixer(self)
self.ui.mixxer.clicked.connect(self.start_thread)
self.ui.Deselect.clicked.connect(self.deselect)
self.load_ui_elements()
self.showFullScreen()
self.ui.keyPressEvent = self.keyPressEvent
self.worker_signals = WorkerSignals()
def show_error_message(self, message):
"""
Displays an error message to the user.
Args:
message (str): The error message to be displayed.
Returns:
None
"""
msg_box = QMessageBox()
msg_box.setIcon(QMessageBox.Icon.Critical)
msg_box.setWindowTitle("Error")
msg_box.setText(message)
msg_box.exec()
def deselect(self):
"""
Deselects all components and resets their properties.
This function does not take any parameters and does not return anything.
"""
for comp in self.components_ports:
self.mixer.higher_precedence_ft_component = None
if comp.original_img != None:
comp.press_pos = None
comp.release_pos = None
comp.current_rect = QRect()
comp.reactivate_drawing_events()
comp.update_display()
def start_thread(self):
"""
Start a new thread for processing.
If there is a running thread, terminate it before starting a new one.
"""
# Check if the pair of images is valid
if not np.all(port.component_data for port in self.components_ports):
logging.error("Nothing to Be Mixed")
return
if len(self.open_order) != 1:
if self.worker_thread and self.worker_thread.is_alive():
logging.info('Terminating the running thread...')
self.worker_thread.cancel()
logging.info('Thread terminated.')
logging.info('Starting a new thread...')
self.worker_signals.canceled.clear()
self.worker_thread = WorkerThread(
5, self.worker_signals, self)
self.worker_thread.start()
else:
logging.error(
msg=f"The user mix one image len(self.open_order)")
return
def map_value(self, value, lower_range, upper_range, lower_range_new, upper_range_new):
"""
Maps a given value from one range to another linearly.
Parameters:
- value (float): The input value to be mapped.
- lower_range (float): The lower bound of the input range.
- upper_range (float): The upper bound of the input range.
- lower_range_new (float): The lower bound of the target range.
- upper_range_new (float): The upper bound of the target range.
Returns:
float: The mapped value in the target range.
"""
mapped_value = ((value - lower_range) * (upper_range_new - lower_range_new) /
(upper_range - lower_range)) + lower_range_new
return mapped_value
def generalize_image_size(self, template_image_ind):
"""
Generalizes the size of all images in the collection based on a template image.
Parameters:
- template_image_ind (int): The index of the template image in the collection.
Description:
Resizes all images in the collection to match the dimensions of the template image.
If an image at the given index is the template itself or if it is None, it is skipped.
This method assumes that the images are represented by objects with 'resized_img'
attribute that can be resized using the 'resize' method, and updates the display
and Fourier Transform (FT) components accordingly.
Note: Make sure that the 'resized_img' attribute is not None for images that need
to be resized.
Returns:
None
"""
for i, image in enumerate(self.image_ports):
if i != template_image_ind and image.original_img is not None:
template_image = self.image_ports[template_image_ind].resized_img
image.resized_img = image.resized_img.resize(
(template_image.width, template_image.height))
image.update_display()
self.components_ports[i].update_FT_components()
def keyPressEvent(self, event):
"""
Handle key events, for example, pressing ESC to exit full screen.
Parameters:
event (QKeyEvent): The key event object.
Returns:
None
"""
# Handle key events, for example, pressing ESC to exit full screen
if event.key() == Qt.Key.Key_Escape:
self.showNormal() # Show the window in normal size
else:
super().keyPressEvent(event)
def load_ui_elements(self):
"""
Load UI elements.
Initializes and configures various UI components such as viewports, combo boxes, sliders, and connects them to appropriate event handlers.
"""
# Define lists of original UI view ports, output ports, component view ports, image combo boxes, mixing combo boxes, and vertical sliders
self.ui_view_ports = [self.ui.original1, self.ui.original2,
self.ui.original3, self.ui.original4]
self.ui_out_ports = [self.ui.output1_port, self.ui.output2_port]
self.ui_view_ports_comp = [
self.ui.component_image1, self.ui.component_image2, self.ui.component_image3, self.ui.component_image4]
self.ui_image_combo_boxes = [
self.ui.combo1, self.ui.combo2, self.ui.combo3, self.ui.combo4]
self.ui_vertical_sliders = [
self.ui.Slider_weight1, self.ui.Slider_weight2, self.ui.Slider_weight3, self.ui.Slider_weight4]
self.ui.vertical_layouts = [
(self.ui.verticalLayout, self.verticalLayout_2), (self.ui.verticalLayout_10,
self.ui.verticalLayout_11), (self.ui.verticalLayout_5, self.ui.verticalLayout_6),
(self.ui.verticalLayout_13, self.ui.verticalLayout_14)
]
self.ui_modes = [self.ui.mode1, self.ui.mode2]
for mode in self.ui_modes:
mode.clicked.connect(self.radio_button_mode_clicked)
self.ui_modes[0].click()
self.out_vertical_layout = [
self.ui.verticalLayout_OP1, self.ui.verticalLayout_OP2]
# Create image viewports and bind browse_image function to the event
self.image_ports.extend([
self.create_image_viewport(self.ui_view_ports[i], lambda event, index=i: self.browse_image(event, index)) for i in range(4)])
# Create FT viewports
self.components_ports.extend([self.create_FT_viewport(
self.ui_view_ports_comp[i]) for i in range(4)])
# Create output viewports
self.out_ports.extend([self.create_output_viewport(
self.ui_out_ports[i]) for i in range(2)])
# Loop through each combo box and associated components_ports
for i, combo_box in enumerate(self.ui_image_combo_boxes):
# Set the combo box and weight slider for the corresponding components_port
self.components_ports[i].combo_box = combo_box
self.components_ports[i].weight_slider = self.ui_vertical_sliders[i]
# Set the minimum and maximum values for the weight slider
self.ui_vertical_sliders[i].setMinimum(1)
self.ui_vertical_sliders[i].setMaximum(100)
# Connect the valueChanged signal of the weight slider to the handle_weight_sliders method of the mixer
self.ui_vertical_sliders[i].valueChanged.connect(
self.mixer.handle_weight_sliders)
# Connect the currentIndexChanged signal of the combo box to the handle_image_combo_boxes_selection method of the components_port
combo_box.currentIndexChanged.connect(
self.components_ports[i].handle_image_combo_boxes_selection)
def radio_button_mode_clicked(self):
sender = self.sender() # Get the radio button that triggered the signal
self.curr_mode = sender.text()
# Add items to the combo box and set the current index to 0
for i, combo_box in enumerate(self.ui_image_combo_boxes):
combo_box.clear()
if self.curr_mode == "Mag and Phase":
combo_box.addItems(
["FT Magnitude", "FT Phase"])
else:
combo_box.addItems(
["FT Real", "FT Imaginary"])
if self.components_ports != []:
print("update ft components")
curr_port = self.components_ports[i]
if self.worker_thread:
self.worker_thread.cancel()
self.worker_signals.canceled.clear()
self.deselect()
if curr_port.original_img != None:
curr_port.update_FT_components()
combo_box.setCurrentIndex(0)
def browse_image(self, event, index: int):
"""
Browse for an image file and set it for the ImageViewport at the specified index.
Args:
event: The event that triggered the image browsing.
index: The index of the ImageViewport to set the image for.
"""
file_filter = "Raw Data (*.png *.jpg *.jpeg *.jfif)"
image_path, _ = QtWidgets.QFileDialog.getOpenFileName(
None, 'Open Signal File', './', filter=file_filter)
if image_path and 0 <= index < len(self.image_ports):
image_port = self.image_ports[index]
if index not in self.open_order:
self.open_order.append(index)
else:
# swap ,and make the one we open the last one
self.open_order[-1], self.open_order[self.open_order.index(
index)] = self.open_order[self.open_order.index(index)], self.open_order[-1]
self.image_processing(index, image_port, image_path)
def image_processing(self, index, image_port, image_path):
"""
Process the image with the given index and update the image parameters.
Parameters:
- index: int - The index of the image to process.
- image_port: ImagePort - The image port object.
- image_path: str - The path to the image file.
Returns:
- None
"""
# Update the viewport image index
image_port.viewport_image_ind = index
# Update the FT index of the component port
self.components_ports[index].viewport_FT_ind = index
# Update the image parameters
image_port.update_image_parameters(image_path)
# store the loading initial size of the component viewport
self.components_ports[index].pre_widget_dim = (
self.components_ports[index].width(), self.components_ports[index].height())
# Update the FT components of the component port
self.components_ports[index].update_FT_components()
# Print the size of the image before resizing
logging.info(f"The size of the image before resizing: {np.array(image_port.original_img).shape}")
# Print the size of the image after resizing
logging.info(f"The size of the image after resizing: {np.array(image_port.resized_img).shape}")
def create_viewport(self, parent, viewport_class, mouse_double_click_event_handler=None):
"""
Creates a viewport of the specified class and adds it to the specified parent widget.
Args:
parent: The parent widget to which the viewport will be added.
viewport_class: The class of the viewport to be created.
mouse_double_click_event_handler: The event handler function to be called when a mouse double-click event occurs (optional).
Returns:
The created viewport.
"""
new_port = viewport_class(self)
layout = QVBoxLayout(parent)
layout.addWidget(new_port)
if mouse_double_click_event_handler:
new_port.mouseDoubleClickEvent = mouse_double_click_event_handler
return new_port
def create_image_viewport(self, parent, mouse_double_click_event_handler):
return self.create_viewport(parent, ImageViewport, mouse_double_click_event_handler)
def create_FT_viewport(self, parent):
return self.create_viewport(parent, FTViewPort)
def create_output_viewport(self, parent):
return self.create_viewport(parent, OutViewPort)
def main():
logging.info(
"----------------------the user open the app-------------------------------------")
app = QtWidgets.QApplication([])
app.setStyleSheet(qdarkstyle.load_stylesheet_pyqt6())
main_window = MainWindow()
main_window.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()