Merge pull request 'image-editor' (#1) from image-editor into main
Reviewed-on: #1
This commit is contained in:
commit
4d6631b03e
5
jezyki-skryptowe/image-editor/editor/.gitignore
vendored
Normal file
5
jezyki-skryptowe/image-editor/editor/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
*.jpg
|
||||||
|
*.png
|
||||||
|
*.JPEG
|
||||||
|
|
||||||
|
__pycache__
|
||||||
56
jezyki-skryptowe/image-editor/editor/DialogsPanel.py
Normal file
56
jezyki-skryptowe/image-editor/editor/DialogsPanel.py
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QPushButton, QGroupBox
|
||||||
|
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.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()
|
||||||
|
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.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.image_mgr.mgr.update(self.dialog.last_processed)
|
||||||
|
self.dialog = None
|
||||||
|
|
||||||
|
def on_rejected(self):
|
||||||
|
self.image_mgr.mgr.refresh()
|
||||||
46
jezyki-skryptowe/image-editor/editor/ImageCanvas.py
Normal file
46
jezyki-skryptowe/image-editor/editor/ImageCanvas.py
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
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
|
||||||
|
empty = True
|
||||||
|
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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
|
||||||
|
self.fitInView(self._pixmapItem, Qt.AspectRatioMode.KeepAspectRatio) # Fit the image to the view
|
||||||
68
jezyki-skryptowe/image-editor/editor/ImageEditor.py
Normal file
68
jezyki-skryptowe/image-editor/editor/ImageEditor.py
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
from PyQt6.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget, QApplication
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ImageCanvas import ImageCanvas
|
||||||
|
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.img_manager = ImageManagePanel()
|
||||||
|
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)
|
||||||
|
self.dialogs_panel.result_ready.connect(self.display_image)
|
||||||
|
|
||||||
|
|
||||||
|
main_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
side_panel = QVBoxLayout()
|
||||||
|
|
||||||
|
side_panel.addWidget(self.img_manager, 1)
|
||||||
|
side_panel.addWidget(self.dialogs_panel, 9)
|
||||||
|
|
||||||
|
preview_panel = QVBoxLayout()
|
||||||
|
preview_panel.addWidget(self.canvas)
|
||||||
|
|
||||||
|
main_layout.addLayout(side_panel,2)
|
||||||
|
main_layout.addLayout(preview_panel, 3)
|
||||||
|
|
||||||
|
self.setLayout(main_layout)
|
||||||
|
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
editor = ImageEditor()
|
||||||
|
editor.show()
|
||||||
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
87
jezyki-skryptowe/image-editor/editor/ImageManagePanel.py
Normal file
87
jezyki-skryptowe/image-editor/editor/ImageManagePanel.py
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
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)
|
||||||
|
|
||||||
|
main_layout = QHBoxLayout()
|
||||||
|
self.setLayout(main_layout)
|
||||||
|
|
||||||
|
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("Save", self)
|
||||||
|
layout.addWidget(self.save_button)
|
||||||
|
self.save_button.clicked.connect(self._save_image)
|
||||||
|
|
||||||
|
self.close_button = QPushButton("Close", self)
|
||||||
|
layout.addWidget(self.close_button)
|
||||||
|
self.close_button.clicked.connect(self._close_image)
|
||||||
|
|
||||||
|
self.undo_button = QPushButton("Undo", self)
|
||||||
|
layout.addWidget(self.undo_button)
|
||||||
|
self.undo_button.clicked.connect(self._undo)
|
||||||
|
|
||||||
|
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):
|
||||||
|
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 (*.png *.jpg *.jpeg *.bmp)")
|
||||||
|
self.open(file_name)
|
||||||
|
|
||||||
|
def _save_image(self):
|
||||||
|
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)}")
|
||||||
60
jezyki-skryptowe/image-editor/editor/ImageManager.py
Normal file
60
jezyki-skryptowe/image-editor/editor/ImageManager.py
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
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
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import numpy as np
|
||||||
|
import cv2
|
||||||
|
from PyQt6.QtCore import pyqtSignal, pyqtSlot, QThread
|
||||||
|
from queue import Queue
|
||||||
|
|
||||||
|
class ImageProcessingWorker(QThread):
|
||||||
|
result_ready = pyqtSignal(np.ndarray)
|
||||||
|
update_values = pyqtSignal(dict)
|
||||||
|
|
||||||
|
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)
|
||||||
|
postprocessed_image = self.postprocess(processed_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)
|
||||||
1
jezyki-skryptowe/image-editor/editor/__init__.py
Executable file
1
jezyki-skryptowe/image-editor/editor/__init__.py
Executable file
|
|
@ -0,0 +1 @@
|
||||||
|
from .image_editor import ImageEditor
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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"
|
||||||
42
jezyki-skryptowe/image-editor/editor/dialogs/FlipDialog.py
Normal file
42
jezyki-skryptowe/image-editor/editor/dialogs/FlipDialog.py
Normal file
|
|
@ -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"
|
||||||
71
jezyki-skryptowe/image-editor/editor/dialogs/HCLDialog.py
Normal file
71
jezyki-skryptowe/image-editor/editor/dialogs/HCLDialog.py
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import numpy as np
|
||||||
|
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QSlider
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
|
||||||
|
import ImageProcessingWorker
|
||||||
|
from .ImageParameterDialog import ImageParameterDialog
|
||||||
|
|
||||||
|
|
||||||
|
class HCLDialog(ImageParameterDialog):
|
||||||
|
def __init__(self, image):
|
||||||
|
super().__init__(image, ImageProcessingWorker.HLSImageProcessingWorker)
|
||||||
|
self.setWindowTitle("HCL Correction")
|
||||||
|
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: 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"))
|
||||||
|
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.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({
|
||||||
|
'hue': self.hue_slider.value(),
|
||||||
|
'chroma': self.chroma_slider.value() / 100.0,
|
||||||
|
'lightness': self.lightness_slider.value() / 100.0
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
image[..., 0] = (image[..., 0] + hue) % 180
|
||||||
|
|
||||||
|
image[..., 1] += (255 * lightness)
|
||||||
|
image[..., 1] = np.clip(image[..., 1], 0, 255)
|
||||||
|
|
||||||
|
image[..., 2] += (255 * chroma)
|
||||||
|
image[..., 2] = np.clip(image[..., 2], 0, 255)
|
||||||
|
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def dialog_name(cls):
|
||||||
|
return "Hue-Chroma-Lightness"
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
from PyQt6.QtWidgets import QDialog, QDialogButtonBox
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
def process_image(self,image, values):
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
131
jezyki-skryptowe/image-editor/editor/dialogs/ResizeDialog.py
Normal file
131
jezyki-skryptowe/image-editor/editor/dialogs/ResizeDialog.py
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
import numpy as np
|
||||||
|
import cv2
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QHBoxLayout, QLineEdit, QCheckBox, QComboBox
|
||||||
|
from PyQt6.QtGui import QIntValidator
|
||||||
|
|
||||||
|
import ImageProcessingWorker
|
||||||
|
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()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.auto_scale_checkbox = QCheckBox("Auto-scale")
|
||||||
|
self.auto_scale_checkbox.setChecked(True)
|
||||||
|
self.auto_scale_checkbox.stateChanged.connect(self.toggle_auto_scale)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
def toggle_auto_scale(self):
|
||||||
|
if self.auto_scale_checkbox.isChecked():
|
||||||
|
self.adjust_height()
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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"
|
||||||
19
jezyki-skryptowe/image-editor/editor/dialogs/__init__.py
Normal file
19
jezyki-skryptowe/image-editor/editor/dialogs/__init__.py
Normal file
|
|
@ -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
|
||||||
|
]
|
||||||
23
jezyki-skryptowe/image-editor/setup.py
Normal file
23
jezyki-skryptowe/image-editor/setup.py
Normal file
|
|
@ -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',
|
||||||
|
)
|
||||||
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