From 382e5bfd9ed1e8f75dcb7ca876082a690bd7ac97 Mon Sep 17 00:00:00 2001 From: Dariusz Majnert Date: Tue, 18 Jun 2024 03:47:47 +0200 Subject: [PATCH] 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