image-editor #1

Merged
xdaro merged 5 commits from image-editor into main 2024-06-18 03:49:25 +02:00
7 changed files with 330 additions and 0 deletions
Showing only changes of commit 2f3f618f41 - Show all commits

View File

@ -0,0 +1,5 @@
*.jpg
*.png
*.JPEG
__pycache__

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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())

View File

@ -0,0 +1,10 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = [
pkgs.python311Packages.numpy
pkgs.python311Packages.pyqt6
pkgs.python311Packages.opencv4
];
}