add all dialogs and add setup
This commit is contained in:
parent
cc0ee4dde1
commit
382e5bfd9e
|
|
@ -1,8 +0,0 @@
|
||||||
from .HCLDialog import HCLDialog
|
|
||||||
from .ResizeDialog import ResizeDialog
|
|
||||||
|
|
||||||
|
|
||||||
DIALOGS = [
|
|
||||||
HCLDialog,
|
|
||||||
ResizeDialog
|
|
||||||
]
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
from PyQt6.QtWidgets import QFileDialog, QWidget, QToolBar, QVBoxLayout, QPushButton
|
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QPushButton, QGroupBox
|
||||||
from PyQt6.QtGui import QIcon
|
|
||||||
from PyQt6.QtCore import pyqtSignal
|
from PyQt6.QtCore import pyqtSignal
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|
@ -12,32 +11,46 @@ class DialogsPanel(QWidget):
|
||||||
|
|
||||||
def __init__(self, image_manager):
|
def __init__(self, image_manager):
|
||||||
super().__init__()
|
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 = []
|
self.dialog_buttons = []
|
||||||
|
|
||||||
|
main_layout = QVBoxLayout()
|
||||||
|
self.setLayout(main_layout)
|
||||||
|
|
||||||
|
operations_group_box = QGroupBox("Operations")
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
self.setLayout(layout)
|
layout.setContentsMargins(75,25,75,25)
|
||||||
|
|
||||||
for DIALOG in DIALOGS:
|
for DIALOG in DIALOGS:
|
||||||
btn = QPushButton(DIALOG.dialog_name(), self)
|
btn = QPushButton(DIALOG.dialog_name(), self)
|
||||||
btn.setProperty(DIALOG_PROPERTY, DIALOG)
|
btn.setProperty(DIALOG_PROPERTY, DIALOG)
|
||||||
btn.clicked.connect(self.open_dialog)
|
btn.clicked.connect(self.open_dialog)
|
||||||
|
btn.setEnabled(False)
|
||||||
|
|
||||||
self.dialog_buttons.append(btn)
|
self.dialog_buttons.append(btn)
|
||||||
layout.addWidget(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):
|
def open_dialog(self):
|
||||||
dialog_factory = self.sender().property(DIALOG_PROPERTY)
|
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.result_ready().connect(lambda img : self.result_ready.emit(img))
|
||||||
self.dialog.accepted.connect(self.on_accepted)
|
self.dialog.accepted.connect(self.on_accepted)
|
||||||
self.dialog.rejected.connect(self.on_rejected)
|
self.dialog.rejected.connect(self.on_rejected)
|
||||||
self.dialog.exec()
|
self.dialog.exec()
|
||||||
|
|
||||||
def on_accepted(self):
|
def on_accepted(self):
|
||||||
self.mgr.update(self.dialog.last_processed)
|
self.image_mgr.mgr.update(self.dialog.last_processed)
|
||||||
self.dialog = None
|
self.dialog = None
|
||||||
|
|
||||||
def on_rejected(self):
|
def on_rejected(self):
|
||||||
self.mgr.refresh()
|
self.image_mgr.mgr.refresh()
|
||||||
|
|
@ -18,7 +18,7 @@ class ImageCanvas(QGraphicsView):
|
||||||
self.setDragMode(self.DragMode.ScrollHandDrag)
|
self.setDragMode(self.DragMode.ScrollHandDrag)
|
||||||
self.setTransformationAnchor(self.ViewportAnchor.AnchorUnderMouse)
|
self.setTransformationAnchor(self.ViewportAnchor.AnchorUnderMouse)
|
||||||
|
|
||||||
def wheelEvent(self, event: QWheelEvent):
|
def wheelEvent(self, event):
|
||||||
zoom_in_factor = 1.25
|
zoom_in_factor = 1.25
|
||||||
zoom_out_factor = 1.0 / zoom_in_factor
|
zoom_out_factor = 1.0 / zoom_in_factor
|
||||||
if event.angleDelta().y() > 0:
|
if event.angleDelta().y() > 0:
|
||||||
|
|
@ -27,7 +27,7 @@ class ImageCanvas(QGraphicsView):
|
||||||
self.scale(zoom_out_factor, zoom_out_factor)
|
self.scale(zoom_out_factor, zoom_out_factor)
|
||||||
|
|
||||||
|
|
||||||
def updatePixmap(self, image: QImage):
|
def updatePixmap(self, image):
|
||||||
pixmap = QPixmap.fromImage(image)
|
pixmap = QPixmap.fromImage(image)
|
||||||
self._pixmapItem.setPixmap(pixmap)
|
self._pixmapItem.setPixmap(pixmap)
|
||||||
if self.empty:
|
if self.empty:
|
||||||
|
|
@ -1,13 +1,7 @@
|
||||||
|
from PyQt6.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget, QApplication
|
||||||
import sys
|
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 ImageCanvas import ImageCanvas
|
||||||
from ImageProcessingWorker import ImageProcessingWorker
|
|
||||||
from ImageManager import ImageManager
|
|
||||||
from ImageManagePanel import ImageManagePanel
|
from ImageManagePanel import ImageManagePanel
|
||||||
from DialogsPanel import DialogsPanel
|
from DialogsPanel import DialogsPanel
|
||||||
|
|
||||||
|
|
@ -25,7 +19,7 @@ class ImageEditor(QWidget):
|
||||||
self.img_manager.on_update.connect(self.display_image)
|
self.img_manager.on_update.connect(self.display_image)
|
||||||
self.img_manager.on_close.connect(lambda : self.canvas.clear())
|
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)
|
self.dialogs_panel.result_ready.connect(self.display_image)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -39,27 +33,36 @@ class ImageEditor(QWidget):
|
||||||
preview_panel = QVBoxLayout()
|
preview_panel = QVBoxLayout()
|
||||||
preview_panel.addWidget(self.canvas)
|
preview_panel.addWidget(self.canvas)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
main_layout.addLayout(side_panel,2)
|
main_layout.addLayout(side_panel,2)
|
||||||
main_layout.addLayout(preview_panel, 3)
|
main_layout.addLayout(preview_panel, 3)
|
||||||
|
|
||||||
self.setLayout(main_layout)
|
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
|
height, width, channel = image.shape
|
||||||
bytes_per_line = 3 * width
|
bytes_per_line = 3 * width
|
||||||
|
|
||||||
self.canvas.updatePixmap(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 main():
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
editor = ImageEditor()
|
editor = ImageEditor()
|
||||||
editor.show()
|
editor.show()
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -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.QtGui import QIcon
|
||||||
from PyQt6.QtCore import pyqtSignal
|
from PyQt6.QtCore import pyqtSignal
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from ImageManager import ImageManager
|
from ImageManager import ImageManager
|
||||||
|
|
||||||
class ImageManagePanel(QWidget):
|
class ImageManagePanel(QWidget):
|
||||||
on_update = pyqtSignal(np.ndarray)
|
on_update = pyqtSignal(np.ndarray)
|
||||||
on_close = pyqtSignal()
|
on_close = pyqtSignal()
|
||||||
|
on_open = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.mgr = ImageManager(self._on_update)
|
self.mgr = ImageManager(self._on_update)
|
||||||
|
|
||||||
layout = QHBoxLayout()
|
main_layout = QHBoxLayout()
|
||||||
self.setLayout(layout)
|
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)
|
layout.addWidget(self.open_button)
|
||||||
self.open_button.clicked.connect(self._open_image)
|
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)
|
layout.addWidget(self.save_button)
|
||||||
self.save_button.clicked.connect(self._save_image)
|
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)
|
layout.addWidget(self.close_button)
|
||||||
self.close_button.clicked.connect(self._close_image)
|
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)
|
layout.addWidget(self.undo_button)
|
||||||
self.undo_button.clicked.connect(self._undo)
|
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)
|
layout.addWidget(self.redo_button)
|
||||||
self.redo_button.clicked.connect(self._redo)
|
self.redo_button.clicked.connect(self._redo)
|
||||||
|
|
||||||
|
file_group_box.setLayout(layout)
|
||||||
|
main_layout.addWidget(file_group_box)
|
||||||
|
|
||||||
self._enable_buttons()
|
self._enable_buttons()
|
||||||
|
|
||||||
def _redo(self):
|
def _redo(self):
|
||||||
|
|
@ -61,13 +67,21 @@ class ImageManagePanel(QWidget):
|
||||||
self.on_close.emit()
|
self.on_close.emit()
|
||||||
|
|
||||||
def _open_image(self):
|
def _open_image(self):
|
||||||
file_name, _ = QFileDialog.getOpenFileName(self, "Open Image File", "", "Image Files (*.jpg *.jpeg *.png)")
|
file_name, _ = QFileDialog.getOpenFileName(self, "Open Image File", "", "Image Files (*.png *.jpg *.jpeg *.bmp)")
|
||||||
if file_name:
|
self.open(file_name)
|
||||||
self.mgr.open(file_name)
|
|
||||||
|
|
||||||
def _save_image(self):
|
def _save_image(self):
|
||||||
file_name, _ = QFileDialog.getSaveFileName(self, "Save Image File", "", "Image Files (*.jpg *.png)")
|
try:
|
||||||
if file_name:
|
file_name, _ = QFileDialog.getSaveFileName(self, "Save Image File", "", "Image Files (*.png *.jpg *.jpeg *.bmp)")
|
||||||
self.mgr.save(file_name)
|
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)}")
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import cv2
|
import cv2
|
||||||
|
|
||||||
class ImageManager():
|
class ImageManager():
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import cv2
|
||||||
from PyQt6.QtCore import pyqtSignal, pyqtSlot, QThread
|
from PyQt6.QtCore import pyqtSignal, pyqtSlot, QThread
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
|
|
||||||
import cv2
|
|
||||||
|
|
||||||
class ImageProcessingWorker(QThread):
|
class ImageProcessingWorker(QThread):
|
||||||
result_ready = pyqtSignal(np.ndarray) # Signal to emit the processed image
|
result_ready = pyqtSignal(np.ndarray)
|
||||||
update_values = pyqtSignal(dict) # Signal to receive new values
|
update_values = pyqtSignal(dict)
|
||||||
|
|
||||||
def __init__(self, image, process_function):
|
def __init__(self, image, process_function):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
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"
|
||||||
|
|
@ -1,19 +1,14 @@
|
||||||
import abc
|
|
||||||
import ImageProcessingWorker
|
|
||||||
|
|
||||||
|
|
||||||
import numpy as np
|
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
|
import ImageProcessingWorker
|
||||||
from PyQt6.QtGui import QPixmap, QImage, QColor, QPainter, QPen
|
|
||||||
from PyQt6.QtCore import Qt, QPoint, QThread
|
|
||||||
|
|
||||||
from .ImageParameterDialog import ImageParameterDialog
|
from .ImageParameterDialog import ImageParameterDialog
|
||||||
|
|
||||||
|
|
||||||
class HCLDialog(ImageParameterDialog):
|
class HCLDialog(ImageParameterDialog):
|
||||||
def __init__(self, hsv_image):
|
def __init__(self, image):
|
||||||
super().__init__(hsv_image, ImageProcessingWorker.HLSImageProcessingWorker)
|
super().__init__(image, ImageProcessingWorker.HLSImageProcessingWorker)
|
||||||
self.setWindowTitle("HCL Correction")
|
self.setWindowTitle("HCL Correction")
|
||||||
self.layout = QVBoxLayout()
|
self.layout = QVBoxLayout()
|
||||||
|
|
||||||
|
|
@ -27,9 +22,9 @@ class HCLDialog(ImageParameterDialog):
|
||||||
self.lightness_slider.setRange(-100, 100)
|
self.lightness_slider.setRange(-100, 100)
|
||||||
|
|
||||||
|
|
||||||
self.label1 = QLabel("Hue Value: 0")
|
self.label1 = QLabel("Hue: 0")
|
||||||
self.label2 = QLabel("Chroma Value: 0")
|
self.label2 = QLabel("Chroma: 0")
|
||||||
self.label3 = QLabel("Lightness Value: 0")
|
self.label3 = QLabel("Lightness: 0")
|
||||||
|
|
||||||
self.hue_slider.valueChanged.connect(lambda value: self.update(self.label1, value, "Hue"))
|
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.chroma_slider.valueChanged.connect(lambda value: self.update(self.label2, value, "Chroma"))
|
||||||
|
|
@ -46,7 +41,7 @@ class HCLDialog(ImageParameterDialog):
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
def update(self, label, value, slider_name):
|
def update(self, label, value, slider_name):
|
||||||
label.setText(f"{slider_name} Value: {value}")
|
label.setText(f"{slider_name}: {value}")
|
||||||
self.send_to_process({
|
self.send_to_process({
|
||||||
'hue': self.hue_slider.value(),
|
'hue': self.hue_slider.value(),
|
||||||
'chroma': self.chroma_slider.value() / 100.0,
|
'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
|
hue = values.get('hue', 0.0) / 2
|
||||||
chroma = values.get('chroma', 0.0)
|
chroma = values.get('chroma', 0.0)
|
||||||
lightness = values.get('lightness', 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)
|
image[..., 1] += (255 * lightness)
|
||||||
hsv_image[..., 1] = np.clip(hsv_image[..., 1], 0, 255)
|
image[..., 1] = np.clip(image[..., 1], 0, 255)
|
||||||
|
|
||||||
hsv_image[..., 2] += (255 * chroma)
|
image[..., 2] += (255 * chroma)
|
||||||
hsv_image[..., 2] = np.clip(hsv_image[..., 2], 0, 255)
|
image[..., 2] = np.clip(image[..., 2], 0, 255)
|
||||||
|
|
||||||
|
|
||||||
return hsv_image
|
return image
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def dialog_name(cls):
|
def dialog_name(cls):
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
import abc
|
from PyQt6.QtWidgets import QDialog, QDialogButtonBox
|
||||||
|
|
||||||
from PyQt6.QtWidgets import QWidget, QDialog, QDialogButtonBox
|
|
||||||
from PyQt6.QtGui import QPixmap, QImage, QColor, QPainter, QPen
|
|
||||||
from PyQt6.QtCore import Qt, QPoint, QThread
|
|
||||||
|
|
||||||
|
|
||||||
class ImageParameterDialog(QDialog):
|
class ImageParameterDialog(QDialog):
|
||||||
|
|
@ -34,9 +30,8 @@ class ImageParameterDialog(QDialog):
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
|
|
||||||
# @abc.abstractmethod
|
|
||||||
def process_image(self,image, values):
|
def process_image(self,image, values):
|
||||||
pass
|
return image
|
||||||
|
|
||||||
|
|
||||||
def result_ready(self):
|
def result_ready(self):
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
import ImageProcessingWorker
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import cv2
|
import cv2
|
||||||
|
|
||||||
from PyQt6.QtWidgets import (QApplication, QLabel, QVBoxLayout, QHBoxLayout, QPushButton, QWidget, QFileDialog, QSlider,
|
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QHBoxLayout, QLineEdit, QCheckBox, QComboBox
|
||||||
QLineEdit, QDialog, QCheckBox, QComboBox)
|
from PyQt6.QtGui import QIntValidator
|
||||||
from PyQt6.QtGui import QPixmap, QImage, QColor, QPainter, QPen, QIntValidator
|
|
||||||
from PyQt6.QtCore import Qt, QPoint, QThread
|
|
||||||
|
|
||||||
|
import ImageProcessingWorker
|
||||||
from .ImageParameterDialog import ImageParameterDialog
|
from .ImageParameterDialog import ImageParameterDialog
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -29,7 +26,6 @@ class ResizeDialog(ImageParameterDialog):
|
||||||
self.setWindowTitle("Resizing")
|
self.setWindowTitle("Resizing")
|
||||||
self.layout = QVBoxLayout()
|
self.layout = QVBoxLayout()
|
||||||
|
|
||||||
# Width input
|
|
||||||
self.width_label = QLabel("Width:")
|
self.width_label = QLabel("Width:")
|
||||||
self.width_field = QLineEdit()
|
self.width_field = QLineEdit()
|
||||||
self.width_field.setPlaceholderText("Enter width")
|
self.width_field.setPlaceholderText("Enter width")
|
||||||
|
|
@ -37,7 +33,6 @@ class ResizeDialog(ImageParameterDialog):
|
||||||
self.width_field.setValidator(QIntValidator(1, 10000))
|
self.width_field.setValidator(QIntValidator(1, 10000))
|
||||||
self.width_field.textEdited.connect(self.width_changed)
|
self.width_field.textEdited.connect(self.width_changed)
|
||||||
|
|
||||||
# Height input
|
|
||||||
self.height_label = QLabel("Height:")
|
self.height_label = QLabel("Height:")
|
||||||
self.height_field = QLineEdit()
|
self.height_field = QLineEdit()
|
||||||
self.height_field.setPlaceholderText("Enter height")
|
self.height_field.setPlaceholderText("Enter height")
|
||||||
|
|
@ -45,17 +40,15 @@ class ResizeDialog(ImageParameterDialog):
|
||||||
self.height_field.setValidator(QIntValidator(1, 10000))
|
self.height_field.setValidator(QIntValidator(1, 10000))
|
||||||
self.height_field.textEdited.connect(self.height_changed)
|
self.height_field.textEdited.connect(self.height_changed)
|
||||||
|
|
||||||
# Auto-scaling checkbox
|
|
||||||
self.auto_scale_checkbox = QCheckBox("Auto-scale")
|
self.auto_scale_checkbox = QCheckBox("Auto-scale")
|
||||||
self.auto_scale_checkbox.setChecked(True)
|
self.auto_scale_checkbox.setChecked(True)
|
||||||
self.auto_scale_checkbox.stateChanged.connect(self.toggle_auto_scale)
|
self.auto_scale_checkbox.stateChanged.connect(self.toggle_auto_scale)
|
||||||
|
|
||||||
# Interpolation method dropdown
|
|
||||||
self.interpolation_label = QLabel("Interpolation Method:")
|
self.interpolation_label = QLabel("Interpolation Method:")
|
||||||
self.interpolation_dropdown = QComboBox()
|
self.interpolation_dropdown = QComboBox()
|
||||||
self.interpolation_dropdown.addItems(list(INTERPOLATION_MAP.keys()))
|
self.interpolation_dropdown.addItems(list(INTERPOLATION_MAP.keys()))
|
||||||
self.interpolation_dropdown.currentIndexChanged.connect(self.update)
|
self.interpolation_dropdown.currentIndexChanged.connect(self.update)
|
||||||
# Layout for input fields
|
|
||||||
input_layout = QHBoxLayout()
|
input_layout = QHBoxLayout()
|
||||||
input_layout.addWidget(self.width_label)
|
input_layout.addWidget(self.width_label)
|
||||||
input_layout.addWidget(self.width_field)
|
input_layout.addWidget(self.width_field)
|
||||||
|
|
@ -71,11 +64,12 @@ class ResizeDialog(ImageParameterDialog):
|
||||||
|
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
self.toggle_auto_scale() # Initially disable the width and height fields
|
self.toggle_auto_scale()
|
||||||
|
|
||||||
def toggle_auto_scale(self):
|
def toggle_auto_scale(self):
|
||||||
if self.auto_scale_checkbox.isChecked():
|
if self.auto_scale_checkbox.isChecked():
|
||||||
self.adjust_height()
|
self.adjust_height()
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
|
||||||
def adjust_height(self):
|
def adjust_height(self):
|
||||||
|
|
@ -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',
|
||||||
|
)
|
||||||
Loading…
Reference in New Issue
Block a user