From 2f3f618f41b3a591162931b1beb528ac5111c2f0 Mon Sep 17 00:00:00 2001 From: Dariusz Majnert Date: Sun, 16 Jun 2024 22:46:30 +0200 Subject: [PATCH] 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