add all dialogs and add setup

This commit is contained in:
Dariusz Majnert 2024-06-18 03:47:47 +02:00
parent cc0ee4dde1
commit 382e5bfd9e
20 changed files with 489 additions and 98 deletions

View File

@ -1,8 +0,0 @@
from .HCLDialog import HCLDialog
from .ResizeDialog import ResizeDialog
DIALOGS = [
HCLDialog,
ResizeDialog
]

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
from collections import deque from collections import deque
import numpy as np
import cv2 import cv2
class ImageManager(): class ImageManager():

View File

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

View File

@ -0,0 +1 @@
from .image_editor import ImageEditor

View File

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

View File

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

View 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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
]

View 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',
)