diff --git a/jezyki-skryptowe/image-editor/DialogsPanel.py b/jezyki-skryptowe/image-editor/DialogsPanel.py new file mode 100644 index 0000000..953487e --- /dev/null +++ b/jezyki-skryptowe/image-editor/DialogsPanel.py @@ -0,0 +1,43 @@ +from PyQt6.QtWidgets import QFileDialog, QWidget, QToolBar, QVBoxLayout, QPushButton +from PyQt6.QtGui import QIcon +from PyQt6.QtCore import pyqtSignal +import numpy as np + +from dialogs import DIALOGS + +DIALOG_PROPERTY = "dialog" + +class DialogsPanel(QWidget): + result_ready = pyqtSignal(np.ndarray) + + def __init__(self, image_manager): + super().__init__() + self.mgr = image_manager + self.dialog_buttons = [] + + layout = QVBoxLayout() + self.setLayout(layout) + + for DIALOG in DIALOGS: + btn = QPushButton(DIALOG.dialog_name(), self) + btn.setProperty(DIALOG_PROPERTY, DIALOG) + btn.clicked.connect(self.open_dialog) + + self.dialog_buttons.append(btn) + layout.addWidget(btn) + + + def open_dialog(self): + dialog_factory = self.sender().property(DIALOG_PROPERTY) + self.dialog = dialog_factory(self.mgr.image()) + self.dialog.result_ready().connect(lambda img : self.result_ready.emit(img)) + self.dialog.accepted.connect(self.on_accepted) + self.dialog.rejected.connect(self.on_rejected) + self.dialog.exec() + + def on_accepted(self): + self.mgr.update(self.dialog.last_processed) + self.dialog = None + + def on_rejected(self): + self.mgr.refresh() diff --git a/jezyki-skryptowe/image-editor/ImageParameterDialog.py b/jezyki-skryptowe/image-editor/ImageParameterDialog.py deleted file mode 100644 index a065d12..0000000 --- a/jezyki-skryptowe/image-editor/ImageParameterDialog.py +++ /dev/null @@ -1,33 +0,0 @@ -import abc -from ImageProcessingWorker import ImageProcessingWorker - -from PyQt6.QtWidgets import QApplication, QLabel, QVBoxLayout, QHBoxLayout, QPushButton, QWidget, QFileDialog, QSlider, QLineEdit, QDialog -from PyQt6.QtGui import QPixmap, QImage, QColor, QPainter, QPen -from PyQt6.QtCore import Qt, QPoint, QThread - - -class ImageParameterDialog(QDialog): - - def __init__(self, hsv_image): - self._hsv_image = hsv_image - self.setup_worker() - super().__init__() - - def setup_worker(self): - self.worker = ImageProcessingWorker(self._hsv_image, self.process_image) - self.update_values_signal = self.worker.update_values - self.update_values_signal.connect(self.worker.process_image) - - self.worker.start() - - - # @abc.abstractmethod - def process_image(self,hsv_image, values): - pass - - - def result_ready(self): - return self.worker.result_ready - - def send_to_process(self, values): - self.update_values_signal.emit(values) \ No newline at end of file diff --git a/jezyki-skryptowe/image-editor/ImageProcessingWorker.py b/jezyki-skryptowe/image-editor/ImageProcessingWorker.py index bcb69d3..858004b 100644 --- a/jezyki-skryptowe/image-editor/ImageProcessingWorker.py +++ b/jezyki-skryptowe/image-editor/ImageProcessingWorker.py @@ -35,11 +35,41 @@ class ImageProcessingWorker(QThread): img = self.image.copy() processed_image = self.process_function(img, values) - processed_image = processed_image.astype('uint8') - rgb_image = cv2.cvtColor(processed_image, cv2.COLOR_HSV2RGB) + postprocessed_image = self.postprocess(processed_image) - self.result_ready.emit(rgb_image) + self.result_ready.emit(postprocessed_image) + + def postprocess(self, processed_image): + return processed_image def stop(self): self.queue(None) self.wait() + + +class RGBImageProcessingWorker(ImageProcessingWorker): + def __init__(self, image, process_function): + img = image.astype('float32') + super().__init__(img, process_function) + + def postprocess(self, processed_image): + return processed_image.astype('uint8') + + +class HSVImageProcessingWorker(ImageProcessingWorker): + def __init__(self, image, process_function): + img = cv2.cvtColor(image, cv2.COLOR_RGB2HSV).astype('float32') + super().__init__(img, process_function) + + def postprocess(self, processed_image): + processed_image = processed_image.astype('uint8') + return cv2.cvtColor(processed_image, cv2.COLOR_HSV2RGB) + +class HLSImageProcessingWorker(ImageProcessingWorker): + def __init__(self, image, process_function): + img = cv2.cvtColor(image, cv2.COLOR_RGB2HLS).astype('float32') + super().__init__(img, process_function) + + def postprocess(self, processed_image): + processed_image = processed_image.astype('uint8') + return cv2.cvtColor(processed_image, cv2.COLOR_HLS2RGB) diff --git a/jezyki-skryptowe/image-editor/HCLDialog.py b/jezyki-skryptowe/image-editor/dialogs/HCLDialog.py similarity index 93% rename from jezyki-skryptowe/image-editor/HCLDialog.py rename to jezyki-skryptowe/image-editor/dialogs/HCLDialog.py index 997edfe..1bb0c59 100644 --- a/jezyki-skryptowe/image-editor/HCLDialog.py +++ b/jezyki-skryptowe/image-editor/dialogs/HCLDialog.py @@ -8,7 +8,7 @@ from PyQt6.QtWidgets import QApplication, QLabel, QVBoxLayout, QHBoxLayout, QPus from PyQt6.QtGui import QPixmap, QImage, QColor, QPainter, QPen from PyQt6.QtCore import Qt, QPoint, QThread -from ImageParameterDialog import ImageParameterDialog +from .ImageParameterDialog import ImageParameterDialog class HCLDialog(ImageParameterDialog): @@ -69,4 +69,8 @@ class HCLDialog(ImageParameterDialog): hsv_image[..., 2] = np.clip(hsv_image[..., 2], 0, 255) - return hsv_image \ No newline at end of file + return hsv_image + + @classmethod + def dialog_name(cls): + return "Hue-Chroma-Lightness" \ No newline at end of file diff --git a/jezyki-skryptowe/image-editor/dialogs/ImageParameterDialog.py b/jezyki-skryptowe/image-editor/dialogs/ImageParameterDialog.py new file mode 100644 index 0000000..4a21c0e --- /dev/null +++ b/jezyki-skryptowe/image-editor/dialogs/ImageParameterDialog.py @@ -0,0 +1,58 @@ +import abc + +from PyQt6.QtWidgets import QWidget, QDialog, QDialogButtonBox +from PyQt6.QtGui import QPixmap, QImage, QColor, QPainter, QPen +from PyQt6.QtCore import Qt, QPoint, QThread + + +class ImageParameterDialog(QDialog): + + def __init__(self, image, worker_factory): + super().__init__() + self._image = image + self.last_processed = image + self.worker_factory = worker_factory + self.setup_worker() + + self.button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) + self.button_box.accepted.connect(self.accept) + self.button_box.rejected.connect(self.reject) + + def setup_worker(self): + self.worker = self.worker_factory(self._image, self.process_image) + self.worker.result_ready.connect(self._update_last_processed) + self.update_values_signal = self.worker.update_values + self.update_values_signal.connect(self.worker.process_image) + + self.worker.start() + + def _update_last_processed(self, new_image): + self.last_processed = new_image + + def _on_accepted(self): + self.worker.stop() + self.accept() + + + # @abc.abstractmethod + def process_image(self,image, values): + pass + + + def result_ready(self): + return self.worker.result_ready + + def send_to_process(self, values): + self.update_values_signal.emit(values) + + def set_accept_enable(self, value): + self.button_box.button(QDialogButtonBox.StandardButton.Ok).setEnabled(value) + + + @classmethod + def dialog_name(cls): + return cls.__name__ + + @classmethod + def dialog_icon(cls): + return None \ No newline at end of file diff --git a/jezyki-skryptowe/image-editor/dialogs/ResizeDialog.py b/jezyki-skryptowe/image-editor/dialogs/ResizeDialog.py new file mode 100644 index 0000000..4e2348b --- /dev/null +++ b/jezyki-skryptowe/image-editor/dialogs/ResizeDialog.py @@ -0,0 +1,137 @@ +import ImageProcessingWorker + +import numpy as np +import cv2 + +from PyQt6.QtWidgets import (QApplication, QLabel, QVBoxLayout, QHBoxLayout, QPushButton, QWidget, QFileDialog, QSlider, + QLineEdit, QDialog, QCheckBox, QComboBox) +from PyQt6.QtGui import QPixmap, QImage, QColor, QPainter, QPen, QIntValidator +from PyQt6.QtCore import Qt, QPoint, QThread + +from .ImageParameterDialog import ImageParameterDialog + + +INTERPOLATION_MAP = { + "Nearest": cv2.INTER_NEAREST, + "Linear": cv2.INTER_LINEAR, + "Area": cv2.INTER_AREA, + "Cubic": cv2.INTER_CUBIC, + "Lanczos4": cv2.INTER_LANCZOS4 +} + +class ResizeDialog(ImageParameterDialog): + def __init__(self, image): + super().__init__(image, ImageProcessingWorker.ImageProcessingWorker) + + self.original_height, self.original_width, _ = image.shape + + + self.setWindowTitle("Resizing") + self.layout = QVBoxLayout() + + # Width input + self.width_label = QLabel("Width:") + self.width_field = QLineEdit() + self.width_field.setPlaceholderText("Enter width") + self.width_field.setText(str(self.original_width)) + self.width_field.setValidator(QIntValidator(1, 10000)) + self.width_field.textEdited.connect(self.width_changed) + + # Height input + self.height_label = QLabel("Height:") + self.height_field = QLineEdit() + self.height_field.setPlaceholderText("Enter height") + self.height_field.setText(str(self.original_height)) + self.height_field.setValidator(QIntValidator(1, 10000)) + self.height_field.textEdited.connect(self.height_changed) + + # Auto-scaling checkbox + self.auto_scale_checkbox = QCheckBox("Auto-scale") + self.auto_scale_checkbox.setChecked(True) + self.auto_scale_checkbox.stateChanged.connect(self.toggle_auto_scale) + + # Interpolation method dropdown + self.interpolation_label = QLabel("Interpolation Method:") + self.interpolation_dropdown = QComboBox() + self.interpolation_dropdown.addItems(list(INTERPOLATION_MAP.keys())) + self.interpolation_dropdown.currentIndexChanged.connect(self.update) + # Layout for input fields + input_layout = QHBoxLayout() + input_layout.addWidget(self.width_label) + input_layout.addWidget(self.width_field) + input_layout.addWidget(self.height_label) + input_layout.addWidget(self.height_field) + + self.layout.addLayout(input_layout) + self.layout.addWidget(self.auto_scale_checkbox) + self.layout.addWidget(self.interpolation_label) + self.layout.addWidget(self.interpolation_dropdown) + + self.layout.addWidget(self.button_box) + + self.setLayout(self.layout) + + self.toggle_auto_scale() # Initially disable the width and height fields + + def toggle_auto_scale(self): + if self.auto_scale_checkbox.isChecked(): + self.adjust_height() + + + def adjust_height(self): + if self.auto_scale_checkbox.isChecked(): + try: + width = int(self.width_field.text()) + height = int((width / self.original_width) * self.original_height) + self.height_field.setText(str(height)) + except ValueError: + self.height_field.clear() + + def adjust_width(self): + if self.auto_scale_checkbox.isChecked(): + try: + height = int(self.height_field.text()) + width = int((height / self.original_height) * self.original_width) + self.width_field.setText(str(width)) + except ValueError: + self.width_field.clear() + + def height_changed(self): + self.adjust_width() + self.update() + + def width_changed(self): + self.adjust_height() + self.update() + + + + def update(self): + width = self.width_field.text() + height = self.height_field.text() + if (not width or not height): + self.set_accept_enable(False) + return + self.send_to_process({ + 'width': int(width), + 'height': int(height), + 'auto_scale': self.auto_scale_checkbox.isChecked(), + 'interpolation': self.interpolation_dropdown.currentText() + }) + + def process_image(self, image, values): + if values['width'] == 0 or values['height'] == 0: + return image + + interpolation = INTERPOLATION_MAP[values['interpolation']] + try: + resized_image = cv2.resize(image, (values['width'], values['height']), interpolation=interpolation) + except: + self.set_accept_enable(False) + return image + self.set_accept_enable(True) + return resized_image + + @classmethod + def dialog_name(cls): + return "Resize" \ No newline at end of file diff --git a/jezyki-skryptowe/image-editor/dialogs/__init__.py b/jezyki-skryptowe/image-editor/dialogs/__init__.py new file mode 100644 index 0000000..5bc229f --- /dev/null +++ b/jezyki-skryptowe/image-editor/dialogs/__init__.py @@ -0,0 +1,8 @@ +from .HCLDialog import HCLDialog +from .ResizeDialog import ResizeDialog + + +DIALOGS = [ + HCLDialog, + ResizeDialog +] \ No newline at end of file diff --git a/jezyki-skryptowe/image-editor/main.py b/jezyki-skryptowe/image-editor/main.py index d3e9f38..6861ce3 100644 --- a/jezyki-skryptowe/image-editor/main.py +++ b/jezyki-skryptowe/image-editor/main.py @@ -7,62 +7,34 @@ import numpy as np from ImageCanvas import ImageCanvas from ImageProcessingWorker import ImageProcessingWorker -from HueDialog import HueDialog - -def process_image_function(image, values): - saturation = values.get('saturation', 0.0) - contrast = values.get('contrast', 1.0) - brightness = values.get('brightness', 0) - - - # Adjust saturation - hsv_image[:, :, 1] += saturation * 255 - hsv_image[:, :, 1] = np.clip(hsv_image[:, :, 1], 0, 255) - - # Adjust brightness and contrast - hsv_image[:, :, 2] = np.clip(hsv_image[:, :, 2] * contrast + brightness, 0, 255) - - - return rgb_image - +from ImageManager import ImageManager +from ImageManagePanel import ImageManagePanel +from DialogsPanel import DialogsPanel class ImageEditor(QWidget): def __init__(self): super().__init__() + self.setWindowTitle("Image Editor") self.setGeometry(100, 100, 800, 600) self.canvas = ImageCanvas() - # self.image_label = QLabel() - # self.image_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.img_manager = ImageManagePanel() + self.img_manager.on_update.connect(self.display_image) + self.img_manager.on_close.connect(lambda : self.canvas.clear()) - self.open_button = QPushButton("Open Image") - self.open_button.clicked.connect(self.open_image) - - self.save_button = QPushButton("Save Image") - self.save_button.clicked.connect(self.save_image) - - self.hcl_button = QPushButton("Hue-Chroma-Lightness") - self.hcl_button.clicked.connect(self.open_hcl) - - - self.resolution_label = QLabel("Resolution:") - self.width_input = QLineEdit() - self.height_input = QLineEdit() - - self.aspect_ratio_label = QLabel("Aspect Ratio:") - self.aspect_ratio_input = QLineEdit() + self.dialogs_panel = DialogsPanel(self.img_manager.mgr) + self.dialogs_panel.result_ready.connect(self.display_image) main_layout = QHBoxLayout() side_panel = QVBoxLayout() - side_panel.addWidget(self.open_button) - side_panel.addWidget(self.save_button) - side_panel.addWidget(self.hcl_button) + side_panel.addWidget(self.img_manager, 1) + side_panel.addWidget(self.dialogs_panel, 9) preview_panel = QVBoxLayout() preview_panel.addWidget(self.canvas) @@ -75,55 +47,15 @@ class ImageEditor(QWidget): self.setLayout(main_layout) - - - - - def open_image(self): - file_name, _ = QFileDialog.getOpenFileName(self, "Open Image File", "", "Image Files (*.jpg *.jpeg *.png)") - if file_name: - self.image = cv2.imread(file_name) - self.image = cv2.cvtColor(self.image, cv2.COLOR_BGR2RGB) - - self.image_as_hsv = cv2.cvtColor(self.image, cv2.COLOR_RGB2HSV).astype('float32') - - self.display_image(self.image, first_load=True) - def display_image(self, image, first_load = False): height, width, channel = image.shape bytes_per_line = 3 * width - q_image = QPixmap.fromImage(QImage(image.data, width, height, bytes_per_line, QImage.Format.Format_RGB888)) + self.canvas.updatePixmap(QImage(image.data, width, height, bytes_per_line, QImage.Format.Format_RGB888)) if first_load: self.canvas.reset() - def save_image(self): - file_name, _ = QFileDialog.getSaveFileName(self, "Save Image File", "", "Image Files (*.jpg *.png)") - if file_name and hasattr(self, 'image'): - cv2.imwrite(file_name, cv2.cvtColor(self.image, cv2.COLOR_RGB2BGR)) - - def open_hcl(self): - self.dialog = HueDialog(self.image_as_hsv) - self.dialog.result_ready().connect(self.display_image) - self.dialog.exec() - - - def update_image(self): - print(self.saturation_slider.value(), self.contrast_slider.value(), self.brightness_slider.value()) - if self.image_as_hsv is not None: - if self.worker is None: - self.setup_worker() - - values = { - 'saturation': self.saturation_slider.value() / 100.0, - 'contrast': self.contrast_slider.value() / 100.0, - 'brightness': self.brightness_slider.value() - } - self.update_values_signal.emit(values) # Emit new values to the worker - - - if __name__ == "__main__":