项目背景和意义
项目背景
随着时代的发展,人们对于修图的需求越来越大,在生活中随处可见处理过的图片,原模原样的图片已经很少见了,在基本的手机自带的相机中,都有着一大堆对于图像进行处理的功能。在学习完本学期的Python课程之后,我对GUI界面编程十分感兴趣,所以打算用PyQt5结合Pillow库,制作一个简单的修图软件。
意义
通过搭建基本的软件GUI界面,和对Pillow库的使用,增强了我的编程能力。在项目开始之前,我对于PyQt5的GUI搭建和Pillow库还不是很了解,需要上网寻找资料,进行自学。这锻炼了我们收集信息的能力。在编程过程中,难免会出现一个又一个的bug,这时候就需要自己进行程序的调试。但有的时候是自己对于程序背后的理解有误,例如对于信号传递机制的理解不够到位等。在不断的处理bug的过程中,增强了我解决问题的能力。遇到问题,不断克服问题。看着一个项目从无到有,被自己一点点的构建起来,这极大的增强了我编程的自信和能力。经过此次开发,我对于项目开发的基本流程有了一定的了解,不再是盲目的开发,想到哪里就写哪里。而是在开始就对程序进行拆分,分成不同的部分,一步一步地分开进行实现。先实现最基本的一部分功能,之后为其添砖加瓦。先实现可用的软件,再继续实现其他。并在程序中注意响应变化,为未来程序的扩展进行准备。在真正的项目中,需求也是在一直变化的,所以我们要注意响应的变化。
需求分析
基本的主界面的分析
主界面主要是由一个QLabel
标签和一个自定义的组件(继承于QTabWidget
)来实现,主窗口继承于QMainWindow
,QLabel
用于显示图片的处理结果,QTabWidget
用来存放对于图片的操作的选项,如调整大小和添加水印等。主页面继承于QMainWidget
,可以实现对应的菜单栏和状态栏,来实现打开图片和保存图片等功能。
对于图片的各个参数的调整
用四个QSlider
来实现对于照片的亮度,长宽和旋转角度的调整。为了让调节的参数更加的直观可见,加入QLabel
来实现对QSlier
数值的显示。可设置QSlider
的范围为-100到100,这样就既可以使参数正向增加也可以负向减少了。对于图像的各个参数,直接传入这样的值是显然不行的,要通过一些基础的方法,来进行数值的转换,转成图片可以接受的参数。其中,亮度可以使用Pillow.ImageEnhance
模块进行操作。大小可以使用PyQt5.QtGui
的QPixmap.resize()
进行操作。旋转可以使用Pillow
的Image.rotate()
进行操作。在调节过程中,难免会有调错,想要重新复原的需求。为了避免手动调整参数到初始值这种繁杂的操作,因此加入了3个重置按钮来重置照片的各个参数。在打开图片之前,同样需要调用这三个重置方法来实现各个数值和QSlider
的复位。
水印
要实现图片的添加水印的功能,添加一个按钮来进行水印的添加和去除。使用Pillow.ImageDraw
模块在图片的左上角添加一个“Watermark”的字样作为水印。并在添加之后,让按钮的显示内容变为“去除水印”,用于去除水印,来灵活地实现水印的添加和去除。
概要和详细设计
代码总框图
其中,Photoshop.py
是软件主体窗体,以及执行部分,Widget_self.py
是自定义控件部分,Variabel.py
是全局变量与常量部分,ProcessPhoto.py
是图片处理方法部分。
各部分框图
菜单栏以及其三个事件
调整图片的各个参数
水印
全局变量与常量
处理图片的Process类
代码实现
python版本以及库版本说明
Python
: 3.9.5 64-bit
PyQt5
: 5.15.4
PyQt5-stubs
: 5.15.2.0
PyQt5-sip
: 12.9.0
PyQt5-Qt5
: 5.15.2
Pillow
: 8.2.0
所使用的关键库
PyQt5
版本5.15.4,是主要的搭建界面所用的库,完成整个界面的搭建,在整个实验过程中使用了其中的多个组件,并完成事件的响应,响应鼠标的各种点击事件。
Pillow
版本8.2.0,Pillow
是python
自带的处理图片的库,整个过程中的图片处理都通过这个库来实现,包括调整大小,水印滤镜等功能。
Sys
保证程序的正常运行所调用的库。
各个文件的说明
Photoshop.py
主要的程序文件,进行主要的界面搭建以及运行,这里是程序的入口文件。
在该文件中,主要进行自定义组件,包括调整图片参数的QSlider
,显示QSlider
数值的QLabel
,进行重置功能和加水印功能的QButton
。并为每个组件链接响应事件的函数,来完成对于图片的不同处理。
Variable.py
用于存放在整个程序中用到的全局变量,各种常量,并为每种全局变量提供.get()
和.set()
方法,用于得到和设置全局变量。各种常量用于规定窗口的大小等。如果将来需求发生变化,需要改变初始窗口的大小,可以直接在这个文件中进行改变,增强了软件响应变化的能力。
ProcessPhoto.py
对图片进行处理的文件,每一次对图片进行处理,都需要进行调用其中的函数,包括改变亮度、调整大小、旋转图片和添加水印。并提供改变的接口,方便其他python
文件进行调用。
关键代码说明
Photoshop.py
初始化自定义组件
1 2 3 4 5 6 7 8 9
| class MyTab(QTabWidget): def __init__(self, parent): super().__init__() self.parent = parent
self.adjust_tab = AdjustTab() self.addTab(self.adjust_tab, "调整参数") self.setMaximumHeight(300)
|
说明:自定义组件MyTab
的实现,继承于QTabWidget
类,为其添加了一个Tab用于操作图片。
初始化软件主窗口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| def initUI(self): self.statusbar = self.statusBar() self.statusbar.showMessage('Ready') openAct = QAction('打开', self) openAct.setShortcut('Ctrl+O') openAct.setStatusTip('打开文件') openAct.triggered.connect(self.openImage) saveAct = QAction('保存', self) saveAct.setShortcut('Ctrl+S') saveAct.setStatusTip('保存文件') saveAct.triggered.connect(self.SaveEvent) exitAct = QAction('退出', self) exitAct.setShortcut('Ctrl+E') exitAct.setStatusTip('退出软件') exitAct.triggered.connect(self.close)
menubar = self.menuBar() fileMenu = menubar.addMenu('文件') fileMenu.addAction(openAct) fileMenu.addAction(saveAct) fileMenu.addAction(exitAct)
imagelabel = QLabel("") Variable.set_imagelabel(imagelabel) imagelabel.setAlignment(Qt.AlignCenter)
self.mytab = MyTab(self)
vbox = QVBoxLayout() vbox.addWidget(imagelabel) vbox.addWidget(self.mytab) main_frame = QWidget() main_frame.setLayout(vbox) self.setCentralWidget(main_frame)
self.resize(Variable.WINDOW_WIDTH, Variable.WINDOW_HEIGHT) self.center() self.setWindowTitle('简易PS') self.setWindowIcon(QIcon('Mini Photoshop/ps.ico')) self.show()
|
说明:实现GUI组件的摆放,并添加菜单栏和状态栏,并为这些东西添加Action,并将其绑定到对应的函数上。
打开文件的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| def openImage(self): imagelabel = Variable.get_imagelabel()
fname, _ = QFileDialog.getOpenFileName( self, '打开图片', '/', "Image files (*.jpg *.png)")
if fname: image = Image.open(fname) Variable.set_image(image)
self.mytab.adjust_tab.reset() self.mytab.adjust_tab.resize() self.mytab.adjust_tab.rerotation()
qimg = ImageQt(image) img_pix = QPixmap.fromImage( qimg, Qt.AutoColor) img_pix = img_pix.scaled( Variable.DEFAULT_WIDTH, Variable.DEFAULT_HEIGHT, Qt.KeepAspectRatio) imagelabel.setPixmap(img_pix) Variable.set_width(img_pix.width()) Variable.set_height(img_pix.height()) process.change_width(0) process.change_height(0) else: pass
|
说明:该函数用于打开图片,当用户点击打开时,就会调用这个函数,用QFileDialog.getOpenFileName
来获得图片的路径,之后调用Pillow
来打开图片,同时更改用来存储图片的全局变量。为了保证多次打开之间不会相互影响,在每次打开图片之后,调用各个组件的重置函数。
保存文件的方法
1 2 3 4 5 6 7 8 9 10
| def SaveEvent(self): filename, _ = QFileDialog.getSaveFileName( self, "文件保存", '/', "Image files (*.jpg *.png)") if filename: image = Variable.get_image() process.change_save(filename) process.process_photo(image) else: pass
|
说明:该函数用来响应保存事件,获得保存路径之后,改变保存标识,调用process_photo()
方法进行保存。
关闭软件的方法
1 2 3 4 5 6 7 8 9 10 11
| def closeEvent(self, event): reply = QMessageBox.question(self, '温馨提示', "你确定要退出吗?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes: event.accept() else: event.ignore()
|
说明:该函数用来响应窗口关闭事件,询问是否退出程序。
设置菜单的功能
1 2 3 4 5 6 7 8 9 10 11 12
| def contextMenuEvent(self, event): cmenu = QMenu(self)
opnAct = cmenu.addAction("打开") saveAct = cmenu.addAction("保存") action = cmenu.exec_(self.mapToGlobal(event.pos()))
if action == opnAct: self.openImage() if action == saveAct: self.SaveEvent()
|
说明:用来响应用户在界面上的鼠标右击事件,显示一个菜单,可以进行图片的打开和保存。
初始化自定义组件的界面和链接各个部件的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| class AdjustTab(QWidget): def __init__(self): super().__init__() self.bright_label = QLabel("亮度") self.bright_label_value = QLabel('0') self.bright_slider = QSlider(Qt.Horizontal, self) self.bright_slider.setMaximum(100) self.bright_slider.setMinimum(-100) self.bright_slider.setValue(0) self.bright_slider.valueChanged[int].connect( self.changeImage) self.high_label = QLabel("高度") self.high_label_value = QLabel('0') self.high_slider = QSlider(Qt.Horizontal, self) self.high_slider.setMaximum(100) self.high_slider.setMinimum(-100) self.high_slider.setValue(0) self.high_slider.valueChanged[int].connect(self.changeImage) self.width_label = QLabel("宽度") self.width_label_value = QLabel('0') self.width_slider = QSlider(Qt.Horizontal, self) self.width_slider.setMaximum(100) self.width_slider.setMinimum(-100) self.width_slider.setValue(0) self.width_slider.valueChanged[int].connect(self.changeImage) self.rotation_label = QLabel("角度") self.rotation_label_value = QLabel('0') self.rotation_slider = QSlider(Qt.Horizontal, self) self.rotation_slider.setMaximum(180) self.rotation_slider.setMinimum(-180) self.rotation_slider.setValue(0) self.rotation_slider.valueChanged[int].connect(self.changeImage) self.reset_button = QPushButton("重置亮度") self.reset_button.clicked.connect(self.reset) self.bind_button = QPushButton("重置大小") self.bind_button.clicked.connect(self.resize) self.rerotation_button = QPushButton("重置角度") self.rerotation_button.clicked.connect(self.rerotation) self.watermark_button = QPushButton("添加水印") self.watermark_button.clicked.connect(self.add_watermark)
hbox1 = QHBoxLayout() hbox1.addWidget(self.bright_label) hbox1.addWidget(self.bright_label_value) hbox2 = QHBoxLayout() hbox2.addWidget(self.high_label) hbox2.addWidget(self.high_label_value) hbox3 = QHBoxLayout() hbox3.addWidget(self.width_label) hbox3.addWidget(self.width_label_value) hbox4 = QHBoxLayout() hbox4.addWidget(self.rotation_label) hbox4.addWidget(self.rotation_label_value) hbox5 = QHBoxLayout() hbox5.addStretch() hbox5.addWidget(self.reset_button) hbox5.addStretch() hbox5.addWidget(self.bind_button) hbox5.addStretch() hbox5.addWidget(self.rerotation_button) hbox5.addStretch() hbox5.addWidget(self.watermark_button) hbox5.addStretch() vbox = QVBoxLayout() vbox.addLayout(hbox1) vbox.addWidget(self.bright_slider) vbox.addLayout(hbox2) vbox.addWidget(self.high_slider) vbox.addLayout(hbox3) vbox.addWidget(self.width_slider) vbox.addLayout(hbox4) vbox.addWidget(self.rotation_slider) vbox.addLayout(hbox5)
self.setLayout(vbox)
|
说明:实现GUI组件的摆放,并添加QSlider
和QLabel
还有QButton
。为它们添加布局,并为这些东西添加Action,将其绑定到对应的函数上。
改变图片的事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| def changeImage(self, value): image = Variable.get_image()
source = self.sender() if source == self.bright_slider: self.bright_label_value.setText(str(value)) bright = (self.bright_slider.value() + 100) / 100 process.change_bright(bright) elif source == self.high_slider: self.high_label_value.setText(str(value)) high = self.high_slider.value() process.change_height(high) elif source == self.width_slider: self.width_label_value.setText(str(value)) width = self.width_slider.value() process.change_width(width) elif source == self.rotation_slider: self.rotation_label_value.setText(str(value)) angle = self.rotation_slider.value() process.change_angle(angle)
process.process_photo(image)
|
说明:本部分响应QSlider
的改变,通过使用Process
中的.change()
方法,来改变图片的属性值。最后通过process_photo()
方法执行这些改变。
添加水印事件
1 2 3 4 5 6 7 8 9 10 11
| def add_watermark(self): image = Variable.get_image() process.change_watermark() process.process_photo(image)
if process.get_watermark(): self.watermark_button.setText("取消水印") else: self.watermark_button.setText("添加水印")
|
说明:本部分响应“增加水印”的QButton
的响应,通过使用Process
中的.change()
方法改变水印标识,再通过process_photo()
方法执行改变。最后还需将QButton
的内容进行更改。
复位事件
1 2 3 4 5 6 7 8 9 10 11 12
| def reset(self): self.bright_slider.setValue(0)
def resize(self): self.width_slider.setValue(0) self.high_slider.setValue(0)
def rerotation(self): self.rotation_slider.setValue(0)
|
说明:本部分包括三个QSlider
的复位事件,响应的是三个QButton
的点击事件,以及图片打开事件。
ProcessPhoto.py
定义一个类来存储图片属性
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Process(): imagelabel = Variable.get_imagelabel()
def __init__(self): self.bright = 1 self.sharpness = 1 self.contrast = 1 self.angle = 0 self.height = 0 self.width = 0 self.watermark = False self.save = ""
|
说明:定义Process
类,专门用于对图像进行处理。因为如果只在其他地方对于图像进行处理,则会导致不同的属性处理的时候,另一属性的处理效果就会消失。所以要进行整体的封装,要将图片的各个属性进行封装,每次进行整体的处理。
改变图片属性值的相关函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| def change_bright(self, bright): self.bright = bright
def change_width(self, value): self.width = Variable.get_width() + value
def change_height(self, value): self.height = Variable.get_height() + value
def change_angle(self, angle): self.angle = angle
def change_watermark(self): self.watermark = not self.watermark
def get_watermark(self): return self.watermark
def change_save(self, path): self.save = path
|
说明:完成对于图片的不同属性的更改,方便之后进行处理
Process_photo()函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| def process_photo(self, image): if image is not None: enhancer = ImageEnhance.Brightness(image) image = enhancer.enhance(self.bright)
if self.watermark: idraw = ImageDraw.Draw(image) text = "Watermark" font = ImageFont.truetype("arial.ttf", size=200) idraw.text((10, 10), text, font=font)
image = image.rotate(self.angle)
imagelabel = Variable.get_imagelabel() qimg = ImageQt(image) img_pix = QPixmap.fromImage(qimg, Qt.AutoColor) img_pix = img_pix.scaled(self.width, self.height) imagelabel.setPixmap(img_pix)
if self.save: img_pix.save(self.save) self.save = False
|
说明:对于图像的处理函数,由于Pillow
库自带的函数性质,可以每次从头进行图片的处理,以达到和GUI界面更好的契合。本函数主要是根据图片的各种参数、标识进行执行修改。
Variable.py
全局常量
1 2 3 4 5
| WINDOW_WIDTH = 1000 WINDOW_HEIGHT = 1000 DEFAULT_WIDTH = 800 DEFAULT_HEIGHT = 600
|
说明:对在整个程序中用到的常量进行定义
Variable类存储全局变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| class Variable: image = None imagelabel = None width = 0 height = 0
def set_width(value): Variable.width = value
def get_width(): return Variable.width
def set_height(value): Variable.height = value
def get_height(): return Variable.height
def set_image(image): Variable.image = image
def get_image(): return Variable.image
def set_imagelabel(imagelabel): Variable.imagelabel = imagelabel
def get_imagelabel(): return Variable.imagelabel
|
说明:保存用到的全局变量,并为其添加.set()
和.get()
函数用于设置和取得对应的全局变量的值。
代码测试
程序运行主界面
读取图片之后的界面
使用各种操作调整图片
保存功能
重置功能和取消水印
本次使用了重置角度功能和取消水印功能
退出功能
结论与未来方向
结论
该项目完成了一些图片处理的基本功能,加深了我对于PyQt5
和Pillow
库的了解。看着一个项目从无到有,自己的编程信心有了很大的提升。并对开发流程及注意事项有了一定的了解。最令我深刻的体会是,在开发中,要尽量解耦合,时时刻刻准备相应变化,让自己的代码在应对不同的需求的时候可以尽量少的改动。
未来方向
虽然已经完成了大部分的功能,但仍然还有很大的扩展空间,例如滤镜、裁剪等功能,这样对于图片的操作更加的自由地实现对于图片的编辑。但由于时间精力原因无法做到更好,十分遗憾。
致谢
感谢常同学的督促与激励。
参考文献与链接
[1] PyQt5 Reference Guide
[2] Pillow — Pillow (PIL Fork) 8.2.0 documentation
[3] PyQt5中文教程
版本管理
本作业已上传至Github以及Gitee,希望各位能点个star再走 :smile:。
GitHub
Gitee