From 2f3f618f41b3a591162931b1beb528ac5111c2f0 Mon Sep 17 00:00:00 2001 From: Dariusz Majnert Date: Sun, 16 Jun 2024 22:46:30 +0200 Subject: [PATCH 1/5] started working on image editor --- jezyki-skryptowe/image-editor/.gitignore | 5 + jezyki-skryptowe/image-editor/HueDialog.py | 68 +++++++++ jezyki-skryptowe/image-editor/ImageCanvas.py | 36 +++++ .../image-editor/ImageParameterDialog.py | 33 +++++ .../image-editor/ImageProcessingWorker.py | 45 ++++++ jezyki-skryptowe/image-editor/main.py | 133 ++++++++++++++++++ jezyki-skryptowe/image-editor/shell.nix | 10 ++ 7 files changed, 330 insertions(+) create mode 100644 jezyki-skryptowe/image-editor/.gitignore create mode 100644 jezyki-skryptowe/image-editor/HueDialog.py create mode 100644 jezyki-skryptowe/image-editor/ImageCanvas.py create mode 100644 jezyki-skryptowe/image-editor/ImageParameterDialog.py create mode 100644 jezyki-skryptowe/image-editor/ImageProcessingWorker.py create mode 100644 jezyki-skryptowe/image-editor/main.py create mode 100644 jezyki-skryptowe/image-editor/shell.nix diff --git a/jezyki-skryptowe/image-editor/.gitignore b/jezyki-skryptowe/image-editor/.gitignore new file mode 100644 index 0000000..2643bb5 --- /dev/null +++ b/jezyki-skryptowe/image-editor/.gitignore @@ -0,0 +1,5 @@ +*.jpg +*.png +*.JPEG + +__pycache__ \ No newline at end of file diff --git a/jezyki-skryptowe/image-editor/HueDialog.py b/jezyki-skryptowe/image-editor/HueDialog.py new file mode 100644 index 0000000..6827036 --- /dev/null +++ b/jezyki-skryptowe/image-editor/HueDialog.py @@ -0,0 +1,68 @@ +import abc +from ImageProcessingWorker import ImageProcessingWorker +import numpy as np + +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 + +from ImageParameterDialog import ImageParameterDialog + + +class HueDialog(ImageParameterDialog): + def __init__(self, hsv_image): + super().__init__(hsv_image) + self.setWindowTitle("Hue Correction Dialog") + self.layout = QVBoxLayout() + + self.hue_slider = QSlider(Qt.Orientation.Horizontal) + self.hue_slider.setRange(-180, 180) + + self.chroma_slider = QSlider(Qt.Orientation.Horizontal) + self.chroma_slider.setRange(-100, 100) + + self.lightness_slider = QSlider(Qt.Orientation.Horizontal) + self.lightness_slider.setRange(-100, 100) + + + self.label1 = QLabel("Hue Value: 0") + self.label2 = QLabel("Chroma Value: 0") + self.label3 = QLabel("Lightness Value: 0") + + self.hue_slider.valueChanged.connect(lambda value: self.update(self.label1, value, "Hue")) + self.chroma_slider.valueChanged.connect(lambda value: self.update(self.label2, value, "Chroma")) + self.lightness_slider.valueChanged.connect(lambda value: self.update(self.label3, value, "Lightness")) + + self.layout.addWidget(self.label1) + self.layout.addWidget(self.hue_slider) + self.layout.addWidget(self.label2) + self.layout.addWidget(self.chroma_slider) + self.layout.addWidget(self.label3) + self.layout.addWidget(self.lightness_slider) + + self.setLayout(self.layout) + + def update(self, label, value, slider_name): + label.setText(f"{slider_name} Value: {value}") + self.send_to_process({ + 'hue': self.hue_slider.value(), + 'chroma': self.chroma_slider.value() / 100.0, + 'lightness': self.lightness_slider.value() / 100.0 + }) + + + + def process_image(self,hsv_image, values): + hue = values.get('hue', 0.0) / 2 + chroma = values.get('chroma', 0.0) + 1.0 + lightness = values.get('lightness', 0.0) + 1.0 + + hsv_image[..., 0] = (hsv_image[..., 0] + hue) % 180 + + # Adjust chroma (saturation) + hsv_image[..., 1] = np.clip(hsv_image[..., 1] * chroma, 0, 255) + + # Adjust lightness (value) + hsv_image[..., 2] = np.clip(hsv_image[..., 2] * lightness, 0, 255) + + return hsv_image \ No newline at end of file diff --git a/jezyki-skryptowe/image-editor/ImageCanvas.py b/jezyki-skryptowe/image-editor/ImageCanvas.py new file mode 100644 index 0000000..e321e1c --- /dev/null +++ b/jezyki-skryptowe/image-editor/ImageCanvas.py @@ -0,0 +1,36 @@ +from PyQt6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem +from PyQt6.QtGui import QPixmap, QWheelEvent, QPainter, QImage +from PyQt6.QtCore import Qt + +class ImageCanvas(QGraphicsView): + _pixmapItem: QGraphicsPixmapItem + + + def __init__(self): + super().__init__() + self.scene = QGraphicsScene() + self.setScene(self.scene) + + self._pixmapItem = QGraphicsPixmapItem() + self.scene.addItem(self._pixmapItem) + + self.setRenderHints(QPainter.RenderHint.Antialiasing | QPainter.RenderHint.SmoothPixmapTransform) + self.setDragMode(self.DragMode.ScrollHandDrag) + self.setTransformationAnchor(self.ViewportAnchor.AnchorUnderMouse) + + def wheelEvent(self, event: QWheelEvent): + zoom_in_factor = 1.25 + zoom_out_factor = 1.0 / zoom_in_factor + if event.angleDelta().y() > 0: + self.scale(zoom_in_factor, zoom_in_factor) + else: + self.scale(zoom_out_factor, zoom_out_factor) + + + def updatePixmap(self, image: QImage): + pixmap = QPixmap.fromImage(image) + self._pixmapItem.setPixmap(pixmap) + + def reset(self): + self.resetTransform() # Reset the zoom level to default + self.fitInView(self._pixmapItem, Qt.AspectRatioMode.KeepAspectRatio) # Fit the image to the view diff --git a/jezyki-skryptowe/image-editor/ImageParameterDialog.py b/jezyki-skryptowe/image-editor/ImageParameterDialog.py new file mode 100644 index 0000000..a065d12 --- /dev/null +++ b/jezyki-skryptowe/image-editor/ImageParameterDialog.py @@ -0,0 +1,33 @@ +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 new file mode 100644 index 0000000..bcb69d3 --- /dev/null +++ b/jezyki-skryptowe/image-editor/ImageProcessingWorker.py @@ -0,0 +1,45 @@ +import numpy as np +from PyQt6.QtCore import pyqtSignal, pyqtSlot, QThread +from queue import Queue + +import cv2 + +class ImageProcessingWorker(QThread): + result_ready = pyqtSignal(np.ndarray) # Signal to emit the processed image + update_values = pyqtSignal(dict) # Signal to receive new values + + def __init__(self, image, process_function): + super().__init__() + self.image = image + self.process_function = process_function + self.values_queue = Queue(maxsize=1) + + @pyqtSlot(dict) + def process_image(self, values): + self.queue(values) + + + def queue(self, value): + if self.values_queue.full(): + try: + self.values_queue.get_nowait() + except Queue.Empty: + pass + self.values_queue.put(value) + + def run(self): + while True: + values = self.values_queue.get() + if values is None: # A way to exit the thread + break + + 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) + + self.result_ready.emit(rgb_image) + + def stop(self): + self.queue(None) + self.wait() diff --git a/jezyki-skryptowe/image-editor/main.py b/jezyki-skryptowe/image-editor/main.py new file mode 100644 index 0000000..d3e9f38 --- /dev/null +++ b/jezyki-skryptowe/image-editor/main.py @@ -0,0 +1,133 @@ +import sys +from PyQt6.QtWidgets import QApplication, QLabel, QVBoxLayout, QHBoxLayout, QPushButton, QWidget, QFileDialog, QSlider, QLineEdit +from PyQt6.QtGui import QPixmap, QImage, QColor, QPainter, QPen +from PyQt6.QtCore import Qt, QPoint, QThread +import cv2 +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 + + +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.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() + + + main_layout = QHBoxLayout() + + side_panel = QVBoxLayout() + + side_panel.addWidget(self.open_button) + side_panel.addWidget(self.save_button) + side_panel.addWidget(self.hcl_button) + + preview_panel = QVBoxLayout() + preview_panel.addWidget(self.canvas) + + + + main_layout.addLayout(side_panel,2) + main_layout.addLayout(preview_panel, 3) + + 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__": + app = QApplication(sys.argv) + editor = ImageEditor() + editor.show() + sys.exit(app.exec()) \ No newline at end of file diff --git a/jezyki-skryptowe/image-editor/shell.nix b/jezyki-skryptowe/image-editor/shell.nix new file mode 100644 index 0000000..6c7eab1 --- /dev/null +++ b/jezyki-skryptowe/image-editor/shell.nix @@ -0,0 +1,10 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + buildInputs = [ + pkgs.python311Packages.numpy + pkgs.python311Packages.pyqt6 + pkgs.python311Packages.opencv4 + ]; + +} \ No newline at end of file From 44f4a9a4bcad2eb2d44c1be668f45565f0e6669b Mon Sep 17 00:00:00 2001 From: Dariusz Majnert Date: Mon, 17 Jun 2024 20:42:01 +0200 Subject: [PATCH 2/5] hcl dialog --- .../{HueDialog.py => HCLDialog.py} | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) rename jezyki-skryptowe/image-editor/{HueDialog.py => HCLDialog.py} (78%) diff --git a/jezyki-skryptowe/image-editor/HueDialog.py b/jezyki-skryptowe/image-editor/HCLDialog.py similarity index 78% rename from jezyki-skryptowe/image-editor/HueDialog.py rename to jezyki-skryptowe/image-editor/HCLDialog.py index 6827036..997edfe 100644 --- a/jezyki-skryptowe/image-editor/HueDialog.py +++ b/jezyki-skryptowe/image-editor/HCLDialog.py @@ -1,5 +1,7 @@ import abc -from ImageProcessingWorker import ImageProcessingWorker +import ImageProcessingWorker + + import numpy as np from PyQt6.QtWidgets import QApplication, QLabel, QVBoxLayout, QHBoxLayout, QPushButton, QWidget, QFileDialog, QSlider, QLineEdit, QDialog @@ -9,10 +11,10 @@ from PyQt6.QtCore import Qt, QPoint, QThread from ImageParameterDialog import ImageParameterDialog -class HueDialog(ImageParameterDialog): +class HCLDialog(ImageParameterDialog): def __init__(self, hsv_image): - super().__init__(hsv_image) - self.setWindowTitle("Hue Correction Dialog") + super().__init__(hsv_image, ImageProcessingWorker.HLSImageProcessingWorker) + self.setWindowTitle("HCL Correction") self.layout = QVBoxLayout() self.hue_slider = QSlider(Qt.Orientation.Horizontal) @@ -39,6 +41,7 @@ class HueDialog(ImageParameterDialog): self.layout.addWidget(self.chroma_slider) self.layout.addWidget(self.label3) self.layout.addWidget(self.lightness_slider) + self.layout.addWidget(self.button_box) self.setLayout(self.layout) @@ -54,15 +57,16 @@ class HueDialog(ImageParameterDialog): def process_image(self,hsv_image, values): hue = values.get('hue', 0.0) / 2 - chroma = values.get('chroma', 0.0) + 1.0 - lightness = values.get('lightness', 0.0) + 1.0 + chroma = values.get('chroma', 0.0) + lightness = values.get('lightness', 0.0) hsv_image[..., 0] = (hsv_image[..., 0] + hue) % 180 - # Adjust chroma (saturation) - hsv_image[..., 1] = np.clip(hsv_image[..., 1] * chroma, 0, 255) + hsv_image[..., 1] += (255 * lightness) + hsv_image[..., 1] = np.clip(hsv_image[..., 1], 0, 255) + + hsv_image[..., 2] += (255 * chroma) + hsv_image[..., 2] = np.clip(hsv_image[..., 2], 0, 255) - # Adjust lightness (value) - hsv_image[..., 2] = np.clip(hsv_image[..., 2] * lightness, 0, 255) return hsv_image \ No newline at end of file From 296d1ef7d464ce8df0f3d0f2dd7220ebf23f871f Mon Sep 17 00:00:00 2001 From: Dariusz Majnert Date: Mon, 17 Jun 2024 23:55:25 +0200 Subject: [PATCH 3/5] image management --- jezyki-skryptowe/image-editor/ImageCanvas.py | 12 ++- .../image-editor/ImageManagePanel.py | 73 +++++++++++++++++++ jezyki-skryptowe/image-editor/ImageManager.py | 61 ++++++++++++++++ 3 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 jezyki-skryptowe/image-editor/ImageManagePanel.py create mode 100644 jezyki-skryptowe/image-editor/ImageManager.py diff --git a/jezyki-skryptowe/image-editor/ImageCanvas.py b/jezyki-skryptowe/image-editor/ImageCanvas.py index e321e1c..7a6cc51 100644 --- a/jezyki-skryptowe/image-editor/ImageCanvas.py +++ b/jezyki-skryptowe/image-editor/ImageCanvas.py @@ -4,7 +4,7 @@ from PyQt6.QtCore import Qt class ImageCanvas(QGraphicsView): _pixmapItem: QGraphicsPixmapItem - + empty = True def __init__(self): super().__init__() @@ -30,6 +30,16 @@ class ImageCanvas(QGraphicsView): def updatePixmap(self, image: QImage): pixmap = QPixmap.fromImage(image) self._pixmapItem.setPixmap(pixmap) + if self.empty: + self.reset() + self.empty = False + + def clear(self): + self.scene.removeItem(self._pixmapItem) + self._pixmapItem = QGraphicsPixmapItem() + self.scene.addItem(self._pixmapItem) + + self.empty = True def reset(self): self.resetTransform() # Reset the zoom level to default diff --git a/jezyki-skryptowe/image-editor/ImageManagePanel.py b/jezyki-skryptowe/image-editor/ImageManagePanel.py new file mode 100644 index 0000000..eaa3fb0 --- /dev/null +++ b/jezyki-skryptowe/image-editor/ImageManagePanel.py @@ -0,0 +1,73 @@ +from PyQt6.QtWidgets import QFileDialog, QWidget, QToolBar, QHBoxLayout, QPushButton +from PyQt6.QtGui import QIcon +from PyQt6.QtCore import pyqtSignal +import numpy as np + +from ImageManager import ImageManager + +class ImageManagePanel(QWidget): + on_update = pyqtSignal(np.ndarray) + on_close = pyqtSignal() + + def __init__(self): + super().__init__() + + self.mgr = ImageManager(self._on_update) + + layout = QHBoxLayout() + self.setLayout(layout) + + self.open_button = QPushButton(QIcon.fromTheme("document-open"), "Open", self) + layout.addWidget(self.open_button) + self.open_button.clicked.connect(self._open_image) + + self.save_button = QPushButton(QIcon("save.png"), "Save", self) + layout.addWidget(self.save_button) + self.save_button.clicked.connect(self._save_image) + + self.close_button = QPushButton(QIcon("close.png"), "Close", self) + layout.addWidget(self.close_button) + self.close_button.clicked.connect(self._close_image) + + self.undo_button = QPushButton(QIcon("undo.png"), "Undo", self) + layout.addWidget(self.undo_button) + self.undo_button.clicked.connect(self._undo) + + self.redo_button = QPushButton(QIcon("redo.png"), "Redo", self) + layout.addWidget(self.redo_button) + self.redo_button.clicked.connect(self._redo) + + self._enable_buttons() + + def _redo(self): + self.mgr.redo() + self._enable_buttons() + + def _undo(self): + self.mgr.undo() + self._enable_buttons() + + def _on_update(self, image): + self.on_update.emit(image) + self._enable_buttons() + + def _enable_buttons(self): + self.undo_button.setEnabled(self.mgr.can_undo()) + self.redo_button.setEnabled(self.mgr.can_redo()) + + + def _close_image(self): + self.mgr.close() + self.on_close.emit() + + def _open_image(self): + file_name, _ = QFileDialog.getOpenFileName(self, "Open Image File", "", "Image Files (*.jpg *.jpeg *.png)") + if file_name: + self.mgr.open(file_name) + + def _save_image(self): + file_name, _ = QFileDialog.getSaveFileName(self, "Save Image File", "", "Image Files (*.jpg *.png)") + if file_name: + self.mgr.save(file_name) + + diff --git a/jezyki-skryptowe/image-editor/ImageManager.py b/jezyki-skryptowe/image-editor/ImageManager.py new file mode 100644 index 0000000..bb8c36b --- /dev/null +++ b/jezyki-skryptowe/image-editor/ImageManager.py @@ -0,0 +1,61 @@ +from collections import deque + +import numpy as np +import cv2 + +class ImageManager(): + + def __init__(self, update_fn): + super().__init__() + self.update_fn = update_fn + self.close() + + def undo(self): + if len(self._undo_deque) == 0: + return + prev = self._undo_deque.pop() + self._redo_deque.append(self._curr_image) + self._curr_image = prev + self.update_fn(self._curr_image) + + def redo(self): + if len(self._redo_deque) == 0: + return + latter = self._redo_deque.pop() + self._undo_deque.append(self._curr_image) + self._curr_image = latter + self.update_fn(self._curr_image) + + def update(self, new_image): + self._undo_deque.append(self._curr_image) + self._redo_deque = deque() + self._curr_image = new_image.copy() + self.update_fn(self._curr_image) + + def open(self, file_name): + self.close() + self._curr_image = cv2.imread(file_name) + self._curr_image = cv2.cvtColor(self._curr_image, cv2.COLOR_BGR2RGB) + self.update_fn(self._curr_image) + + + def close(self): + self._undo_deque = deque() + self._redo_deque = deque() + self._curr_image = None + + def save(self, file_name): + if self._curr_image is not None: + cv2.imwrite(file_name, cv2.cvtColor(self._curr_image, cv2.COLOR_RGB2BGR)) + + def refresh(self): + self.update_fn(self._curr_image) + + def image(self): + return self._curr_image + + def can_redo(self): + return len(self._redo_deque) > 0 + + def can_undo(self): + return len(self._undo_deque) > 0 \ No newline at end of file From cc0ee4dde107670c1aa53281176bc9f0c39cf56b Mon Sep 17 00:00:00 2001 From: Dariusz Majnert Date: Mon, 17 Jun 2024 23:55:32 +0200 Subject: [PATCH 4/5] dialogs --- jezyki-skryptowe/image-editor/DialogsPanel.py | 43 ++++++ .../image-editor/ImageParameterDialog.py | 33 ----- .../image-editor/ImageProcessingWorker.py | 36 ++++- .../image-editor/{ => dialogs}/HCLDialog.py | 8 +- .../dialogs/ImageParameterDialog.py | 58 ++++++++ .../image-editor/dialogs/ResizeDialog.py | 137 ++++++++++++++++++ .../image-editor/dialogs/__init__.py | 8 + jezyki-skryptowe/image-editor/main.py | 92 ++---------- 8 files changed, 297 insertions(+), 118 deletions(-) create mode 100644 jezyki-skryptowe/image-editor/DialogsPanel.py delete mode 100644 jezyki-skryptowe/image-editor/ImageParameterDialog.py rename jezyki-skryptowe/image-editor/{ => dialogs}/HCLDialog.py (93%) create mode 100644 jezyki-skryptowe/image-editor/dialogs/ImageParameterDialog.py create mode 100644 jezyki-skryptowe/image-editor/dialogs/ResizeDialog.py create mode 100644 jezyki-skryptowe/image-editor/dialogs/__init__.py 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__": From 382e5bfd9ed1e8f75dcb7ca876082a690bd7ac97 Mon Sep 17 00:00:00 2001 From: Dariusz Majnert Date: Tue, 18 Jun 2024 03:47:47 +0200 Subject: [PATCH 5/5] add all dialogs and add setup --- .../image-editor/dialogs/__init__.py | 8 -- .../image-editor/{ => editor}/.gitignore | 0 .../image-editor/{ => editor}/DialogsPanel.py | 27 ++++-- .../image-editor/{ => editor}/ImageCanvas.py | 4 +- .../{main.py => editor/ImageEditor.py} | 39 +++++---- .../{ => editor}/ImageManagePanel.py | 50 +++++++---- .../image-editor/{ => editor}/ImageManager.py | 1 - .../{ => editor}/ImageProcessingWorker.py | 7 +- .../image-editor/editor/__init__.py | 1 + .../dialogs/BrightnessContrastDialog.py | 54 ++++++++++++ .../editor/dialogs/ColorBalanceDialog.py | 75 ++++++++++++++++ .../image-editor/editor/dialogs/FlipDialog.py | 42 +++++++++ .../{ => editor}/dialogs/HCLDialog.py | 37 ++++---- .../dialogs/ImageParameterDialog.py | 9 +- .../{ => editor}/dialogs/ResizeDialog.py | 18 ++-- .../editor/dialogs/RotationDialog.py | 87 +++++++++++++++++++ .../editor/dialogs/SaturationDialog.py | 40 +++++++++ .../editor/dialogs/TemperatureDialog.py | 46 ++++++++++ .../image-editor/editor/dialogs/__init__.py | 19 ++++ jezyki-skryptowe/image-editor/setup.py | 23 +++++ 20 files changed, 489 insertions(+), 98 deletions(-) delete mode 100644 jezyki-skryptowe/image-editor/dialogs/__init__.py rename jezyki-skryptowe/image-editor/{ => editor}/.gitignore (100%) rename jezyki-skryptowe/image-editor/{ => editor}/DialogsPanel.py (53%) rename jezyki-skryptowe/image-editor/{ => editor}/ImageCanvas.py (94%) rename jezyki-skryptowe/image-editor/{main.py => editor/ImageEditor.py} (69%) rename jezyki-skryptowe/image-editor/{ => editor}/ImageManagePanel.py (52%) rename jezyki-skryptowe/image-editor/{ => editor}/ImageManager.py (98%) rename jezyki-skryptowe/image-editor/{ => editor}/ImageProcessingWorker.py (93%) create mode 100755 jezyki-skryptowe/image-editor/editor/__init__.py create mode 100644 jezyki-skryptowe/image-editor/editor/dialogs/BrightnessContrastDialog.py create mode 100644 jezyki-skryptowe/image-editor/editor/dialogs/ColorBalanceDialog.py create mode 100644 jezyki-skryptowe/image-editor/editor/dialogs/FlipDialog.py rename jezyki-skryptowe/image-editor/{ => editor}/dialogs/HCLDialog.py (65%) rename jezyki-skryptowe/image-editor/{ => editor}/dialogs/ImageParameterDialog.py (86%) rename jezyki-skryptowe/image-editor/{ => editor}/dialogs/ResizeDialog.py (88%) create mode 100644 jezyki-skryptowe/image-editor/editor/dialogs/RotationDialog.py create mode 100644 jezyki-skryptowe/image-editor/editor/dialogs/SaturationDialog.py create mode 100644 jezyki-skryptowe/image-editor/editor/dialogs/TemperatureDialog.py create mode 100644 jezyki-skryptowe/image-editor/editor/dialogs/__init__.py create mode 100644 jezyki-skryptowe/image-editor/setup.py diff --git a/jezyki-skryptowe/image-editor/dialogs/__init__.py b/jezyki-skryptowe/image-editor/dialogs/__init__.py deleted file mode 100644 index 5bc229f..0000000 --- a/jezyki-skryptowe/image-editor/dialogs/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .HCLDialog import HCLDialog -from .ResizeDialog import ResizeDialog - - -DIALOGS = [ - HCLDialog, - ResizeDialog -] \ No newline at end of file diff --git a/jezyki-skryptowe/image-editor/.gitignore b/jezyki-skryptowe/image-editor/editor/.gitignore similarity index 100% rename from jezyki-skryptowe/image-editor/.gitignore rename to jezyki-skryptowe/image-editor/editor/.gitignore diff --git a/jezyki-skryptowe/image-editor/DialogsPanel.py b/jezyki-skryptowe/image-editor/editor/DialogsPanel.py similarity index 53% rename from jezyki-skryptowe/image-editor/DialogsPanel.py rename to jezyki-skryptowe/image-editor/editor/DialogsPanel.py index 953487e..a52a911 100644 --- a/jezyki-skryptowe/image-editor/DialogsPanel.py +++ b/jezyki-skryptowe/image-editor/editor/DialogsPanel.py @@ -1,5 +1,4 @@ -from PyQt6.QtWidgets import QFileDialog, QWidget, QToolBar, QVBoxLayout, QPushButton -from PyQt6.QtGui import QIcon +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QPushButton, QGroupBox from PyQt6.QtCore import pyqtSignal import numpy as np @@ -12,32 +11,46 @@ class DialogsPanel(QWidget): def __init__(self, image_manager): super().__init__() - self.mgr = image_manager + self.image_mgr = image_manager + self.image_mgr.on_close.connect(lambda : self.buttons_set_enabled(False)) + self.image_mgr.on_open.connect(lambda : self.buttons_set_enabled(True)) + self.dialog_buttons = [] + main_layout = QVBoxLayout() + self.setLayout(main_layout) + + operations_group_box = QGroupBox("Operations") layout = QVBoxLayout() - self.setLayout(layout) + layout.setContentsMargins(75,25,75,25) for DIALOG in DIALOGS: btn = QPushButton(DIALOG.dialog_name(), self) btn.setProperty(DIALOG_PROPERTY, DIALOG) btn.clicked.connect(self.open_dialog) + btn.setEnabled(False) self.dialog_buttons.append(btn) layout.addWidget(btn) + operations_group_box.setLayout(layout) + main_layout.addWidget(operations_group_box) + + def buttons_set_enabled(self, state): + for btn in self.dialog_buttons: + btn.setEnabled(state) def open_dialog(self): dialog_factory = self.sender().property(DIALOG_PROPERTY) - self.dialog = dialog_factory(self.mgr.image()) + self.dialog = dialog_factory(self.image_mgr.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.image_mgr.mgr.update(self.dialog.last_processed) self.dialog = None def on_rejected(self): - self.mgr.refresh() + self.image_mgr.mgr.refresh() diff --git a/jezyki-skryptowe/image-editor/ImageCanvas.py b/jezyki-skryptowe/image-editor/editor/ImageCanvas.py similarity index 94% rename from jezyki-skryptowe/image-editor/ImageCanvas.py rename to jezyki-skryptowe/image-editor/editor/ImageCanvas.py index 7a6cc51..c5cb4d9 100644 --- a/jezyki-skryptowe/image-editor/ImageCanvas.py +++ b/jezyki-skryptowe/image-editor/editor/ImageCanvas.py @@ -18,7 +18,7 @@ class ImageCanvas(QGraphicsView): self.setDragMode(self.DragMode.ScrollHandDrag) self.setTransformationAnchor(self.ViewportAnchor.AnchorUnderMouse) - def wheelEvent(self, event: QWheelEvent): + def wheelEvent(self, event): zoom_in_factor = 1.25 zoom_out_factor = 1.0 / zoom_in_factor if event.angleDelta().y() > 0: @@ -27,7 +27,7 @@ class ImageCanvas(QGraphicsView): self.scale(zoom_out_factor, zoom_out_factor) - def updatePixmap(self, image: QImage): + def updatePixmap(self, image): pixmap = QPixmap.fromImage(image) self._pixmapItem.setPixmap(pixmap) if self.empty: diff --git a/jezyki-skryptowe/image-editor/main.py b/jezyki-skryptowe/image-editor/editor/ImageEditor.py similarity index 69% rename from jezyki-skryptowe/image-editor/main.py rename to jezyki-skryptowe/image-editor/editor/ImageEditor.py index 6861ce3..b9ecece 100644 --- a/jezyki-skryptowe/image-editor/main.py +++ b/jezyki-skryptowe/image-editor/editor/ImageEditor.py @@ -1,13 +1,7 @@ +from PyQt6.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget, QApplication import sys -from PyQt6.QtWidgets import QApplication, QLabel, QVBoxLayout, QHBoxLayout, QPushButton, QWidget, QFileDialog, QSlider, QLineEdit -from PyQt6.QtGui import QPixmap, QImage, QColor, QPainter, QPen -from PyQt6.QtCore import Qt, QPoint, QThread -import cv2 -import numpy as np from ImageCanvas import ImageCanvas -from ImageProcessingWorker import ImageProcessingWorker -from ImageManager import ImageManager from ImageManagePanel import ImageManagePanel from DialogsPanel import DialogsPanel @@ -25,7 +19,7 @@ class ImageEditor(QWidget): self.img_manager.on_update.connect(self.display_image) self.img_manager.on_close.connect(lambda : self.canvas.clear()) - self.dialogs_panel = DialogsPanel(self.img_manager.mgr) + self.dialogs_panel = DialogsPanel(self.img_manager) self.dialogs_panel.result_ready.connect(self.display_image) @@ -39,27 +33,36 @@ class ImageEditor(QWidget): preview_panel = QVBoxLayout() preview_panel.addWidget(self.canvas) - - main_layout.addLayout(side_panel,2) main_layout.addLayout(preview_panel, 3) self.setLayout(main_layout) - def display_image(self, image, first_load = False): + self.setAcceptDrops(True) + + def dragEnterEvent(self, event): + if event.mimeData().hasUrls(): + event.acceptProposedAction() + + def dropEvent(self, event): + files = [url.toLocalFile() for url in event.mimeData().urls()] + if files: + self.img_manager.open(files[0]) + + + def display_image(self, image): height, width, channel = image.shape bytes_per_line = 3 * width - self.canvas.updatePixmap(QImage(image.data, width, height, bytes_per_line, QImage.Format.Format_RGB888)) - if first_load: - self.canvas.reset() - - -if __name__ == "__main__": +def main(): app = QApplication(sys.argv) editor = ImageEditor() editor.show() - sys.exit(app.exec()) \ No newline at end of file + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() diff --git a/jezyki-skryptowe/image-editor/ImageManagePanel.py b/jezyki-skryptowe/image-editor/editor/ImageManagePanel.py similarity index 52% rename from jezyki-skryptowe/image-editor/ImageManagePanel.py rename to jezyki-skryptowe/image-editor/editor/ImageManagePanel.py index eaa3fb0..a0f494a 100644 --- a/jezyki-skryptowe/image-editor/ImageManagePanel.py +++ b/jezyki-skryptowe/image-editor/editor/ImageManagePanel.py @@ -1,42 +1,48 @@ -from PyQt6.QtWidgets import QFileDialog, QWidget, QToolBar, QHBoxLayout, QPushButton +from PyQt6.QtWidgets import QFileDialog, QWidget, QHBoxLayout, QPushButton, QMessageBox, QGroupBox from PyQt6.QtGui import QIcon from PyQt6.QtCore import pyqtSignal import numpy as np from ImageManager import ImageManager - class ImageManagePanel(QWidget): on_update = pyqtSignal(np.ndarray) on_close = pyqtSignal() - + on_open = pyqtSignal() + def __init__(self): super().__init__() self.mgr = ImageManager(self._on_update) - layout = QHBoxLayout() - self.setLayout(layout) + main_layout = QHBoxLayout() + self.setLayout(main_layout) - self.open_button = QPushButton(QIcon.fromTheme("document-open"), "Open", self) + file_group_box = QGroupBox("File") + layout = QHBoxLayout() + + self.open_button = QPushButton("Open", self) layout.addWidget(self.open_button) self.open_button.clicked.connect(self._open_image) - self.save_button = QPushButton(QIcon("save.png"), "Save", self) + self.save_button = QPushButton("Save", self) layout.addWidget(self.save_button) self.save_button.clicked.connect(self._save_image) - self.close_button = QPushButton(QIcon("close.png"), "Close", self) + self.close_button = QPushButton("Close", self) layout.addWidget(self.close_button) self.close_button.clicked.connect(self._close_image) - self.undo_button = QPushButton(QIcon("undo.png"), "Undo", self) + self.undo_button = QPushButton("Undo", self) layout.addWidget(self.undo_button) self.undo_button.clicked.connect(self._undo) - self.redo_button = QPushButton(QIcon("redo.png"), "Redo", self) + self.redo_button = QPushButton("Redo", self) layout.addWidget(self.redo_button) self.redo_button.clicked.connect(self._redo) + file_group_box.setLayout(layout) + main_layout.addWidget(file_group_box) + self._enable_buttons() def _redo(self): @@ -61,13 +67,21 @@ class ImageManagePanel(QWidget): self.on_close.emit() def _open_image(self): - file_name, _ = QFileDialog.getOpenFileName(self, "Open Image File", "", "Image Files (*.jpg *.jpeg *.png)") - if file_name: - self.mgr.open(file_name) - + file_name, _ = QFileDialog.getOpenFileName(self, "Open Image File", "", "Image Files (*.png *.jpg *.jpeg *.bmp)") + self.open(file_name) + def _save_image(self): - file_name, _ = QFileDialog.getSaveFileName(self, "Save Image File", "", "Image Files (*.jpg *.png)") - if file_name: - self.mgr.save(file_name) - + try: + file_name, _ = QFileDialog.getSaveFileName(self, "Save Image File", "", "Image Files (*.png *.jpg *.jpeg *.bmp)") + if file_name: + self.mgr.save(file_name) + except Exception as e: + QMessageBox.critical(self, "Error Saving File", f"An error occurred while saving the file:\n{str(e)}") + def open(self, file_name): + try: + if file_name: + self.mgr.open(file_name) + self.on_open.emit() + except Exception as e: + QMessageBox.critical(self, "Error Opening File", f"An error occurred while opening the file:\n{str(e)}") diff --git a/jezyki-skryptowe/image-editor/ImageManager.py b/jezyki-skryptowe/image-editor/editor/ImageManager.py similarity index 98% rename from jezyki-skryptowe/image-editor/ImageManager.py rename to jezyki-skryptowe/image-editor/editor/ImageManager.py index bb8c36b..83d10c6 100644 --- a/jezyki-skryptowe/image-editor/ImageManager.py +++ b/jezyki-skryptowe/image-editor/editor/ImageManager.py @@ -1,6 +1,5 @@ from collections import deque -import numpy as np import cv2 class ImageManager(): diff --git a/jezyki-skryptowe/image-editor/ImageProcessingWorker.py b/jezyki-skryptowe/image-editor/editor/ImageProcessingWorker.py similarity index 93% rename from jezyki-skryptowe/image-editor/ImageProcessingWorker.py rename to jezyki-skryptowe/image-editor/editor/ImageProcessingWorker.py index 858004b..54fd2c7 100644 --- a/jezyki-skryptowe/image-editor/ImageProcessingWorker.py +++ b/jezyki-skryptowe/image-editor/editor/ImageProcessingWorker.py @@ -1,12 +1,11 @@ import numpy as np +import cv2 from PyQt6.QtCore import pyqtSignal, pyqtSlot, QThread from queue import Queue -import cv2 - class ImageProcessingWorker(QThread): - result_ready = pyqtSignal(np.ndarray) # Signal to emit the processed image - update_values = pyqtSignal(dict) # Signal to receive new values + result_ready = pyqtSignal(np.ndarray) + update_values = pyqtSignal(dict) def __init__(self, image, process_function): super().__init__() diff --git a/jezyki-skryptowe/image-editor/editor/__init__.py b/jezyki-skryptowe/image-editor/editor/__init__.py new file mode 100755 index 0000000..d6712a9 --- /dev/null +++ b/jezyki-skryptowe/image-editor/editor/__init__.py @@ -0,0 +1 @@ +from .image_editor import ImageEditor \ No newline at end of file diff --git a/jezyki-skryptowe/image-editor/editor/dialogs/BrightnessContrastDialog.py b/jezyki-skryptowe/image-editor/editor/dialogs/BrightnessContrastDialog.py new file mode 100644 index 0000000..bf3b207 --- /dev/null +++ b/jezyki-skryptowe/image-editor/editor/dialogs/BrightnessContrastDialog.py @@ -0,0 +1,54 @@ +import numpy as np +import cv2 +from PyQt6.QtWidgets import QVBoxLayout, QLabel, QSlider +from PyQt6.QtCore import Qt + +from .ImageParameterDialog import ImageParameterDialog +import ImageProcessingWorker + +class BrightnessContrastDialog(ImageParameterDialog): + def __init__(self, image): + super().__init__(image, ImageProcessingWorker.RGBImageProcessingWorker) + self.setWindowTitle("Brightness/Contrast Adjustment") + self.layout = QVBoxLayout() + + self.brightness_slider = QSlider(Qt.Orientation.Horizontal) + self.brightness_slider.setRange(-127, 127) + + self.contrast_slider = QSlider(Qt.Orientation.Horizontal) + self.contrast_slider.setRange(-127, 127) + + + self.label1 = QLabel("Brightness: 0") + self.label2 = QLabel("Contrast: 0") + + + self.brightness_slider.valueChanged.connect(lambda value: self.update(self.label1, value, "Brightness")) + self.contrast_slider.valueChanged.connect(lambda value: self.update(self.label2, value, "Contrast")) + + self.layout.addWidget(self.label1) + self.layout.addWidget(self.brightness_slider) + self.layout.addWidget(self.label2) + self.layout.addWidget(self.contrast_slider) + self.layout.addWidget(self.button_box) + + self.setLayout(self.layout) + + def update(self, label, value, slider_name): + label.setText(f"{slider_name}: {value}") + self.send_to_process({ + 'brightness': self.brightness_slider.value(), + 'contrast': self.contrast_slider.value(), + }) + + def process_image(self, image, values): + brightness = values.get('brightness', 0.0) + 1 + contrast = values.get('contrast', 0.0) + 1 + c = (259 * (contrast + 255)) / (255 * (259 - contrast)) + + scaled = c * (image - 128) + 128 + brightness + return np.clip(scaled, 0, 255) + + @classmethod + def dialog_name(cls): + return "Brightness-Contrast" \ No newline at end of file diff --git a/jezyki-skryptowe/image-editor/editor/dialogs/ColorBalanceDialog.py b/jezyki-skryptowe/image-editor/editor/dialogs/ColorBalanceDialog.py new file mode 100644 index 0000000..56f0318 --- /dev/null +++ b/jezyki-skryptowe/image-editor/editor/dialogs/ColorBalanceDialog.py @@ -0,0 +1,75 @@ +import numpy as np +import cv2 +from PyQt6.QtWidgets import QVBoxLayout, QLabel, QSlider, QHBoxLayout, QGroupBox +from PyQt6.QtCore import Qt + +from .ImageParameterDialog import ImageParameterDialog +import ImageProcessingWorker + +class ColorBalanceDialog(ImageParameterDialog): + def __init__(self, image): + super().__init__(image, ImageProcessingWorker.RGBImageProcessingWorker) + self.setWindowTitle("Color Balance Adjustment") + self.layout = QVBoxLayout() + + self.sliders = {} + self.labels = {} + self.channel_names = ['Red', 'Green', 'Blue'] + self.tone_names = ['Shadows', 'Midtones', 'Highlights'] + + for tone in self.tone_names: + tone_box = QGroupBox(tone) + tone_layout = QVBoxLayout() + + for channel in self.channel_names: + slider = QSlider(Qt.Orientation.Horizontal) + slider.setRange(-100, 100) + slider.setValue(0) + label = QLabel(f"{channel}: 0") + slider.valueChanged.connect(lambda value, lbl=label, ch=channel, tn=tone: self.update(lbl, value, ch, tn)) + + self.sliders[(channel, tone)] = slider + self.labels[(channel, tone)] = label + + tone_layout.addWidget(label) + tone_layout.addWidget(slider) + + tone_box.setLayout(tone_layout) + self.layout.addWidget(tone_box) + + self.layout.addWidget(self.button_box) + self.setLayout(self.layout) + + def update(self, label, value, channel, tone): + label.setText(f"{channel}: {value}") + self.send_to_process(self.collect_values()) + + def collect_values(self): + values = {} + for channel in self.channel_names: + for tone in self.tone_names: + values[(channel, tone)] = self.sliders[(channel, tone)].value() + return values + + def process_image(self, image, values): + img = image / 255.0 + + luminance = 0.299 * image[..., 0] + 0.587 * image[..., 1] + 0.114 * image[..., 2] + + shadows_mask = luminance < 85 + midtones_mask = (luminance >= 85) & (luminance <= 170) + highlights_mask = luminance > 170 + + for channel_idx, channel in enumerate(self.channel_names): + for tone, mask in zip(self.tone_names, [shadows_mask, midtones_mask, highlights_mask]): + adjustment = values[(channel, tone)] + if adjustment != 0: + factor = (259 * (adjustment + 255)) / (255 * (259 - adjustment)) + img[..., channel_idx] = np.where(mask, img[..., channel_idx] * factor, img[..., channel_idx]) + + img = np.clip(img * 255, 0, 255) + return img + + @classmethod + def dialog_name(cls): + return "Color Balance" diff --git a/jezyki-skryptowe/image-editor/editor/dialogs/FlipDialog.py b/jezyki-skryptowe/image-editor/editor/dialogs/FlipDialog.py new file mode 100644 index 0000000..af51104 --- /dev/null +++ b/jezyki-skryptowe/image-editor/editor/dialogs/FlipDialog.py @@ -0,0 +1,42 @@ +from PyQt6.QtWidgets import QVBoxLayout, QLabel, QCheckBox +import cv2 + +from .ImageParameterDialog import ImageParameterDialog +import ImageProcessingWorker + +class FlipDialog(ImageParameterDialog): + def __init__(self, image): + super().__init__(image, ImageProcessingWorker.ImageProcessingWorker) + self.setWindowTitle("Flip Image") + self.layout = QVBoxLayout() + + self.horizontal_flip_checkbox = QCheckBox("Flip Horizontally") + self.horizontal_flip_checkbox.stateChanged.connect(self.update) + + self.vertical_flip_checkbox = QCheckBox("Flip Vertically") + self.vertical_flip_checkbox.stateChanged.connect(self.update) + + self.layout.addWidget(QLabel("Select flip options:")) + self.layout.addWidget(self.horizontal_flip_checkbox) + self.layout.addWidget(self.vertical_flip_checkbox) + self.layout.addWidget(self.button_box) + + self.setLayout(self.layout) + + def update(self): + self.send_to_process({ + 'flip_horizontal': self.horizontal_flip_checkbox.isChecked(), + 'flip_vertical': self.vertical_flip_checkbox.isChecked() + }) + + def process_image(self, image, values): + if values['flip_horizontal']: + image = cv2.flip(image, 1) + if values['flip_vertical']: + image = cv2.flip(image, 0) + + return image + + @classmethod + def dialog_name(cls): + return "Flip Image" \ No newline at end of file diff --git a/jezyki-skryptowe/image-editor/dialogs/HCLDialog.py b/jezyki-skryptowe/image-editor/editor/dialogs/HCLDialog.py similarity index 65% rename from jezyki-skryptowe/image-editor/dialogs/HCLDialog.py rename to jezyki-skryptowe/image-editor/editor/dialogs/HCLDialog.py index 1bb0c59..58e3187 100644 --- a/jezyki-skryptowe/image-editor/dialogs/HCLDialog.py +++ b/jezyki-skryptowe/image-editor/editor/dialogs/HCLDialog.py @@ -1,19 +1,14 @@ -import abc -import ImageProcessingWorker - - import numpy as np +from PyQt6.QtWidgets import QLabel, QVBoxLayout, QSlider +from PyQt6.QtCore import Qt -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 - +import ImageProcessingWorker from .ImageParameterDialog import ImageParameterDialog class HCLDialog(ImageParameterDialog): - def __init__(self, hsv_image): - super().__init__(hsv_image, ImageProcessingWorker.HLSImageProcessingWorker) + def __init__(self, image): + super().__init__(image, ImageProcessingWorker.HLSImageProcessingWorker) self.setWindowTitle("HCL Correction") self.layout = QVBoxLayout() @@ -27,9 +22,9 @@ class HCLDialog(ImageParameterDialog): self.lightness_slider.setRange(-100, 100) - self.label1 = QLabel("Hue Value: 0") - self.label2 = QLabel("Chroma Value: 0") - self.label3 = QLabel("Lightness Value: 0") + self.label1 = QLabel("Hue: 0") + self.label2 = QLabel("Chroma: 0") + self.label3 = QLabel("Lightness: 0") self.hue_slider.valueChanged.connect(lambda value: self.update(self.label1, value, "Hue")) self.chroma_slider.valueChanged.connect(lambda value: self.update(self.label2, value, "Chroma")) @@ -46,7 +41,7 @@ class HCLDialog(ImageParameterDialog): self.setLayout(self.layout) def update(self, label, value, slider_name): - label.setText(f"{slider_name} Value: {value}") + label.setText(f"{slider_name}: {value}") self.send_to_process({ 'hue': self.hue_slider.value(), 'chroma': self.chroma_slider.value() / 100.0, @@ -55,21 +50,21 @@ class HCLDialog(ImageParameterDialog): - def process_image(self,hsv_image, values): + def process_image(self,image, values): hue = values.get('hue', 0.0) / 2 chroma = values.get('chroma', 0.0) lightness = values.get('lightness', 0.0) - hsv_image[..., 0] = (hsv_image[..., 0] + hue) % 180 + image[..., 0] = (image[..., 0] + hue) % 180 - hsv_image[..., 1] += (255 * lightness) - hsv_image[..., 1] = np.clip(hsv_image[..., 1], 0, 255) + image[..., 1] += (255 * lightness) + image[..., 1] = np.clip(image[..., 1], 0, 255) - hsv_image[..., 2] += (255 * chroma) - hsv_image[..., 2] = np.clip(hsv_image[..., 2], 0, 255) + image[..., 2] += (255 * chroma) + image[..., 2] = np.clip(image[..., 2], 0, 255) - return hsv_image + return image @classmethod def dialog_name(cls): diff --git a/jezyki-skryptowe/image-editor/dialogs/ImageParameterDialog.py b/jezyki-skryptowe/image-editor/editor/dialogs/ImageParameterDialog.py similarity index 86% rename from jezyki-skryptowe/image-editor/dialogs/ImageParameterDialog.py rename to jezyki-skryptowe/image-editor/editor/dialogs/ImageParameterDialog.py index 4a21c0e..8f93297 100644 --- a/jezyki-skryptowe/image-editor/dialogs/ImageParameterDialog.py +++ b/jezyki-skryptowe/image-editor/editor/dialogs/ImageParameterDialog.py @@ -1,8 +1,4 @@ -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 +from PyQt6.QtWidgets import QDialog, QDialogButtonBox class ImageParameterDialog(QDialog): @@ -34,9 +30,8 @@ class ImageParameterDialog(QDialog): self.accept() - # @abc.abstractmethod def process_image(self,image, values): - pass + return image def result_ready(self): diff --git a/jezyki-skryptowe/image-editor/dialogs/ResizeDialog.py b/jezyki-skryptowe/image-editor/editor/dialogs/ResizeDialog.py similarity index 88% rename from jezyki-skryptowe/image-editor/dialogs/ResizeDialog.py rename to jezyki-skryptowe/image-editor/editor/dialogs/ResizeDialog.py index 4e2348b..57b633b 100644 --- a/jezyki-skryptowe/image-editor/dialogs/ResizeDialog.py +++ b/jezyki-skryptowe/image-editor/editor/dialogs/ResizeDialog.py @@ -1,13 +1,10 @@ -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 PyQt6.QtWidgets import QLabel, QVBoxLayout, QHBoxLayout, QLineEdit, QCheckBox, QComboBox +from PyQt6.QtGui import QIntValidator +import ImageProcessingWorker from .ImageParameterDialog import ImageParameterDialog @@ -29,7 +26,6 @@ class ResizeDialog(ImageParameterDialog): self.setWindowTitle("Resizing") self.layout = QVBoxLayout() - # Width input self.width_label = QLabel("Width:") self.width_field = QLineEdit() self.width_field.setPlaceholderText("Enter width") @@ -37,7 +33,6 @@ class ResizeDialog(ImageParameterDialog): 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") @@ -45,17 +40,15 @@ class ResizeDialog(ImageParameterDialog): 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) @@ -71,11 +64,12 @@ class ResizeDialog(ImageParameterDialog): self.setLayout(self.layout) - self.toggle_auto_scale() # Initially disable the width and height fields + self.toggle_auto_scale() def toggle_auto_scale(self): if self.auto_scale_checkbox.isChecked(): self.adjust_height() + self.update() def adjust_height(self): diff --git a/jezyki-skryptowe/image-editor/editor/dialogs/RotationDialog.py b/jezyki-skryptowe/image-editor/editor/dialogs/RotationDialog.py new file mode 100644 index 0000000..77ee8c9 --- /dev/null +++ b/jezyki-skryptowe/image-editor/editor/dialogs/RotationDialog.py @@ -0,0 +1,87 @@ +from PyQt6.QtWidgets import QLabel, QVBoxLayout, QHBoxLayout, QLineEdit, QCheckBox, QSlider +from PyQt6.QtGui import QIntValidator +from PyQt6.QtCore import Qt +from .ImageParameterDialog import ImageParameterDialog +import ImageProcessingWorker +import numpy as np +import cv2 + +class RotationDialog(ImageParameterDialog): + def __init__(self, image): + super().__init__(image, ImageProcessingWorker.ImageProcessingWorker) + self.setWindowTitle("Rotation") + self.layout = QVBoxLayout() + + self.angle_label = QLabel("Angle:") + self.angle_field = QLineEdit() + self.angle_field.setText("0") + self.angle_field.setPlaceholderText("Enter rotation angle") + self.angle_field.setValidator(QIntValidator(-360, 360)) + self.angle_field.textEdited.connect(self.angle_text_changed) + + self.angle_slider = QSlider(Qt.Orientation.Horizontal) + self.angle_slider.setRange(-360, 360) + self.angle_slider.setTickInterval(90) + self.angle_slider.setTickPosition(QSlider.TickPosition.TicksBelow) + self.angle_slider.valueChanged.connect(self.angle_slider_changed) + + self.keep_size_checkbox = QCheckBox("Keep original size") + self.keep_size_checkbox.setChecked(True) + self.keep_size_checkbox.stateChanged.connect(self.update) + + input_layout = QHBoxLayout() + input_layout.addWidget(self.angle_label) + input_layout.addWidget(self.angle_field) + + self.layout.addLayout(input_layout) + self.layout.addWidget(self.angle_slider) + self.layout.addWidget(self.keep_size_checkbox) + self.layout.addWidget(self.button_box) + + self.setLayout(self.layout) + + def angle_text_changed(self, text): + if text: + angle = int(text) + self.angle_slider.setValue(angle) + self.update() + + def angle_slider_changed(self, value): + self.angle_field.setText(str(value)) + self.update() + + def update(self): + angle = self.angle_field.text() + if not angle: + self.set_accept_enable(False) + return + self.send_to_process({ + 'angle': float(angle), + 'keep_size': self.keep_size_checkbox.isChecked() + }) + + def process_image(self, image, values): + angle = values['angle'] + keep_size = values['keep_size'] + + height, width = image.shape[:2] + center = (width // 2, height // 2) + matrix = cv2.getRotationMatrix2D(center, angle, 1.0) + + if keep_size: + rotated_image = cv2.warpAffine(image, matrix, (width, height)) + else: + cos = np.abs(matrix[0, 0]) + sin = np.abs(matrix[0, 1]) + new_width = int((height * sin) + (width * cos)) + new_height = int((height * cos) + (width * sin)) + matrix[0, 2] += (new_width / 2) - center[0] + matrix[1, 2] += (new_height / 2) - center[1] + rotated_image = cv2.warpAffine(image, matrix, (new_width, new_height)) + self.set_accept_enable(True) + + return rotated_image + + @classmethod + def dialog_name(cls): + return "Rotate" \ No newline at end of file diff --git a/jezyki-skryptowe/image-editor/editor/dialogs/SaturationDialog.py b/jezyki-skryptowe/image-editor/editor/dialogs/SaturationDialog.py new file mode 100644 index 0000000..5d2af01 --- /dev/null +++ b/jezyki-skryptowe/image-editor/editor/dialogs/SaturationDialog.py @@ -0,0 +1,40 @@ +import numpy as np +from PyQt6.QtWidgets import QVBoxLayout, QLabel, QSlider +from PyQt6.QtCore import Qt +from .ImageParameterDialog import ImageParameterDialog +import ImageProcessingWorker + +class SaturationDialog(ImageParameterDialog): + def __init__(self, image): + super().__init__(image, ImageProcessingWorker.HSVImageProcessingWorker) + self.setWindowTitle("Saturation Adjustment") + self.layout = QVBoxLayout() + + self.saturation_slider = QSlider(Qt.Orientation.Horizontal) + self.saturation_slider.setRange(-100, 100) + + self.label = QLabel("Saturation: 0") + self.saturation_slider.valueChanged.connect(lambda value: self.update(self.label, value)) + + self.layout.addWidget(self.label) + self.layout.addWidget(self.saturation_slider) + self.layout.addWidget(self.button_box) + + self.setLayout(self.layout) + + def update(self, label, value): + label.setText(f"Saturation: {value}") + self.send_to_process({ + 'saturation': self.saturation_slider.value() / 100.0 + }) + + def process_image(self, image, values): + saturation = values.get('saturation', 0.0) + + image[..., 1] = np.clip(image[..., 1] * (1 + saturation), 0, 255) + + return image + + @classmethod + def dialog_name(cls): + return "Saturation" \ No newline at end of file diff --git a/jezyki-skryptowe/image-editor/editor/dialogs/TemperatureDialog.py b/jezyki-skryptowe/image-editor/editor/dialogs/TemperatureDialog.py new file mode 100644 index 0000000..74f03f0 --- /dev/null +++ b/jezyki-skryptowe/image-editor/editor/dialogs/TemperatureDialog.py @@ -0,0 +1,46 @@ +from PyQt6.QtWidgets import QVBoxLayout, QLabel, QSlider +from PyQt6.QtCore import Qt +from .ImageParameterDialog import ImageParameterDialog +import ImageProcessingWorker +import numpy as np + +class TemperatureAdjustmentDialog(ImageParameterDialog): + def __init__(self, image): + super().__init__(image, ImageProcessingWorker.RGBImageProcessingWorker) + self.setWindowTitle("Temperature Adjustment") + self.layout = QVBoxLayout() + + self.temperature_slider = QSlider(Qt.Orientation.Horizontal) + self.temperature_slider.setRange(-500, 500) + self.temperature_slider.setValue(0) + self.temperature_slider.setTickInterval(10) + self.temperature_slider.setTickPosition(QSlider.TickPosition.TicksBelow) + + self.label = QLabel("Temperature: 0") + + self.temperature_slider.valueChanged.connect(self.update_temperature) + + self.layout.addWidget(self.label) + self.layout.addWidget(self.temperature_slider) + self.layout.addWidget(self.button_box) + self.setLayout(self.layout) + + def update_temperature(self, value): + self.label.setText(f"Temperature: {value}") + self.send_to_process({'temperature': value}) + + def process_image(self, image, values): + temperature = values.get('temperature', 0) + + increment = temperature * 0.1 + + image[..., 2] += increment * -1 + image[..., 0] += increment + + image = np.clip(image, 0, 255) + + return image.astype(np.uint8) + + @classmethod + def dialog_name(cls): + return "Temperature Adjustment" \ No newline at end of file diff --git a/jezyki-skryptowe/image-editor/editor/dialogs/__init__.py b/jezyki-skryptowe/image-editor/editor/dialogs/__init__.py new file mode 100644 index 0000000..b33c37c --- /dev/null +++ b/jezyki-skryptowe/image-editor/editor/dialogs/__init__.py @@ -0,0 +1,19 @@ +from .HCLDialog import HCLDialog +from .ResizeDialog import ResizeDialog +from .SaturationDialog import SaturationDialog +from .BrightnessContrastDialog import BrightnessContrastDialog +from .ColorBalanceDialog import ColorBalanceDialog +from .TemperatureDialog import TemperatureAdjustmentDialog +from .RotationDialog import RotationDialog +from .FlipDialog import FlipDialog + +DIALOGS = [ + ColorBalanceDialog, + TemperatureAdjustmentDialog, + HCLDialog, + SaturationDialog, + BrightnessContrastDialog, + ResizeDialog, + RotationDialog, + FlipDialog +] \ No newline at end of file diff --git a/jezyki-skryptowe/image-editor/setup.py b/jezyki-skryptowe/image-editor/setup.py new file mode 100644 index 0000000..085bf04 --- /dev/null +++ b/jezyki-skryptowe/image-editor/setup.py @@ -0,0 +1,23 @@ +from setuptools import setup, find_packages + +setup( + name="image-editor", + version="1.0.0", + packages=find_packages(), + install_requires=[ + "numpy", + "opencv-python", + "PyQt6" + ], + entry_points={ + 'console_scripts': [ + 'run-image-editor=editor.ImageEditor:main', + ], + }, + classifiers=[ + "Programming Language :: Python :: 3", + "License :: MIT License", + "Operating System :: OS Independent", + ], + python_requires='>=3.6', +) \ No newline at end of file