started working on image editor
This commit is contained in:
parent
8f6d2a37f2
commit
2f3f618f41
5
jezyki-skryptowe/image-editor/.gitignore
vendored
Normal file
5
jezyki-skryptowe/image-editor/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
*.jpg
|
||||
*.png
|
||||
*.JPEG
|
||||
|
||||
__pycache__
|
||||
68
jezyki-skryptowe/image-editor/HueDialog.py
Normal file
68
jezyki-skryptowe/image-editor/HueDialog.py
Normal 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
|
||||
36
jezyki-skryptowe/image-editor/ImageCanvas.py
Normal file
36
jezyki-skryptowe/image-editor/ImageCanvas.py
Normal 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
|
||||
33
jezyki-skryptowe/image-editor/ImageParameterDialog.py
Normal file
33
jezyki-skryptowe/image-editor/ImageParameterDialog.py
Normal 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)
|
||||
45
jezyki-skryptowe/image-editor/ImageProcessingWorker.py
Normal file
45
jezyki-skryptowe/image-editor/ImageProcessingWorker.py
Normal 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()
|
||||
133
jezyki-skryptowe/image-editor/main.py
Normal file
133
jezyki-skryptowe/image-editor/main.py
Normal 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())
|
||||
10
jezyki-skryptowe/image-editor/shell.nix
Normal file
10
jezyki-skryptowe/image-editor/shell.nix
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{ pkgs ? import <nixpkgs> {} }:
|
||||
|
||||
pkgs.mkShell {
|
||||
buildInputs = [
|
||||
pkgs.python311Packages.numpy
|
||||
pkgs.python311Packages.pyqt6
|
||||
pkgs.python311Packages.opencv4
|
||||
];
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user