【没钱买ps,PyQt自己写】Day 30 - final project - 3 / 来搞一个自己的 photoshop 吧!把每个方法封装起来制作出还原功能吧!(结合 PyQt + OpenCV)

看完这篇文章你会得到的成果图

此篇文章的范例程序码 github

https://github.com/howarder3/ironman2021_PyQt5_photoshop/tree/main/day30_final_project_package_method

复习之前的内容 (前情提要)

完整版请参考:

我们之前讨论到了我们是如何设计程序的程序架构,
以大概念来说,我们主轴还是围绕在

  • UI
  • controller
  • start

三大面向,而 UI 我们已经透过 Qt desinger 设定完成,
而 start 没什麽好说。
我们开始着重讨论 controller 的细节。

独立「图像本身」与「图像处理方法」,额外设计图像处理介面。

我们选择独立「图片本身」与「图片处理方法」,
我们想避免把所有图片的功能全部都做在我们的图像中心 (image center) 里面,
这样会变成一个超级巨大的 class (又名为 god class),
功能太多之後要维护一个特定功能太难了,所以我们才独立「图像处理方法」进行操作。

这部分是套用 design pattern 的设计原则 (使用 Interface Segregation Principle(ISP) 介面隔离原则)
我们可以把介面分离出来,更方便之後功能的维护。

介面设计与继承方法

套用 design pattern 後 (使用 Interface Segregation Principle(ISP) 介面隔离原则)

套用 design pattern 的 Interface Segregation Principle(ISP) 介面隔离原则後,
我们把「修改图片的方法」这个介面独立出来,更方便我们维护「图片修改」的部分。

而继承的部分,从变更图片的「所有共通方法 -> 滑条类方法/笔类方法 -> 各项细节方法」。

我们在 day29 的时候,介绍了每个功能的实作细节

day28,我们讲解了我们系统的大架构,与 UI 的设计。
而 day 29,我们把每个细节的功能全部都介绍完了。

那今天我们还有什麽事情可以做呢?

今天我们要来谈封装方法,建构出「步骤的流程」

我们仔细观察不论是 小画家 或 photoshop 的程序,
都会有提供「还原」或「重做」的功能。

我们该如何在我们自己的 photoshop 实作出这种功能呢?

分析「还原」或「重做」的功能,别人是怎麽做出来的?

保存图片流

首先,如果用最简单的方法,也许我们可以存图片?
也就是说,我们开一个 queue,「每更新一次画面,就存一个 frame」,
这样听起来简单暴力,但是可行XDDD

保存方法流 (保存变化量)

不过,如果我们再更仔细的观察,因为如果「把每张图都存起来」,
势必会消耗大量的储存空间,因此应该会有更好的优化方法,
我们思考有没有可能对方存的只有「原图 + 修改步骤」,
换句话说,也就是「原图 + (图片的变化量)」。

我们可以常常在还原功能那边看到「上一个步骤」具体进行了什麽的操作,
而不是「保存的上一张图片」,因此,我们也乾脆来实作一个保存「变更的方法」。

如果这样子做,我们就可以省下大量的储存图片空间,
而且我们也可以直接知道上一个「步骤内容」是什麽。

介面微更新

我们新增了可以记录步骤的框框,「还原」或「重做」的按钮。

实作保存方法的机制

我们宣告了一个新的 class method_steps_recoder

class method_steps_recoder(object):
    def __init__(self, text_recordsteps):
        self.method_steps = []
        self.text_recordsteps = text_recordsteps

    def add_each_method_step(self, each_method_step):
        self.method_steps.append(each_method_step)

    def update_recordsteps(self):
        msg = f"All saved steps: \n"
        for idx, ele in enumerate(self.method_steps):
            msg +=(f"{idx+1}: {ele}\n")
        self.text_recordsteps.setText(msg)

稍微想了一下,这个保存机制初始化的时间,
应该与图片刚初始化的时间同时,
因此我们也在 class image_center 开始读档的时候,
宣告 method_steps_recoder(),同时传入要修改的参数。

self.method_steps_recoder = method_steps_recoder(self.ui.text_recordsteps) # record steps

因为介面继承的关系,我们可以轻松地增加记录功能

我们上面已经把介面继承写得非常有架构了,因此这次要记录步骤的功能,
我们只需要去更新上层的介面即可。

我们在 slider_method_interface 新增一个函数 append_each_method_step(),
并修改 slider_release_event(滑条释放的时间),会呼叫这个函数,保存这次的更新内容。

就完成了这部分的所有功能了!

class slider_method_interface(method_interface):
    # final update back to image center (not necessary, for double check)
    def slider_release_event(self):
        img = self.setimage(self.tmp_origin_img)
        self.append_each_method_step() # append all the methods include variables in to method_steps_recoder
        self.image_center.update_img(img)

    def append_each_method_step(self):
        self.image_center.method_steps_recoder.add_each_method_step(self) # append all the methods include variables in to method_steps_recoder

结果

因此,现在只要有滑条值的变化,都会启动一次纪录 (每拖曳并放开滑鼠时纪录一次),
如下所示:

【问题】然而,光是这样的架构还不足以我们实现「还原」或「重做」

实作到此的我,发现目前想要实现出「还原」或「重做」,
还存在一些问题:

1. 还原上一步时,该如何复原目前图片的变更?

依照演算法,很多对图片的变更可能都是「对图片的破坏性变更」,
也就是说,替图片「减少一笔」的难度远比「增加一笔」高出非常多。

2. 还原上一步时,哪些画面上的零件也需要还原?

例如像是滑条显示、步骤显示,这些可能需要都被还原。
目前实作上只有处理图片的架构比较完整,
但这些内容并没有被好好的封装起来,导致还原上有困难。

3. 滑条对应的内容,是「一个 instance」 而不是 「new 一个新的 instance」

这大概是我目前系统架构做不出还原功能的致命伤。
因为滑条只有一个,而照理来说「每进行一次滑条的变动」,
就应该要 「new 一个新的滑条变动的 instance」,
因为目前这部分我是绑死再一起的,所以这边确实应该还要再拆分。

预期未来解决问题的方法

上面三个问题,也可以浓缩成一个设计问题。
基本上我会考虑将机制改为,存「原图 + 所有变化的方法」,
与上述不同的是不只是存「图片的变化」,
这次连 UI 当下的状态可能也需要被储存下来。

因此,未来如果要继续实作这部分的功能。
我会考虑把「储存的方法」改为存「UI 变化设定 & 当下图的图片变化」

系统运作的逻辑会类似保存:

原图 -> 图面&UI变化 -> 图面&UI变化 -> 图面&UI变化...

所以我们纪录的东西反而是「步骤」,
至於还原的时候,可以以当时保存最旧的图片,
依照「步骤」全部重新运算,
应该就能够如我们预期的完成「还原」或「重做」的功能。

优化效能

此外,我们要处理一下我们系统的效能优化,
昨天的程序执行後,如果是不够强大的 CPU,或解析度太大的图片,

会没有办法应付「移动滑条」造成「图片的连续变化」运算。

【修改】我们暂时先移除,随着滑条图片一起变动的功能

主要是因为,滑条跟图片一起变,图片解析度太大,
我们电脑处理不来,会导致程序严重卡在运算上。

於是我们就先移除这个「动态演饰」的效果,
我们只保留「变动前的样子」、「变动後的结果」。

透过这样的方式大幅减少中间过程的对电脑效能上的负担。

修改程序码部分

  • 各个 setsliderlabel 改为不更新原图,只更新 label。
    # trigger function, get your signal from here
    def setsliderlabel(self):
        self.label.setText(f"{self.prefix}{self.slider.value():+}")
        # self.update_img()  # for the efficiency reason, we don't let the picture change with our slider
  • 在 class slider_method_interface 中,更新 slider_release_event (释放滑鼠时),
    变成只在释放滑鼠时更新图片,由於介面继承设计的关系,
    其他的功能也会被同步修改完成。

这样就完成了我们的效能优化。

class slider_method_interface(method_interface):
# final update back to image center (not necessary, for double check)
    def slider_release_event(self):
        img = self.setimage(self.tmp_origin_img)
        self.append_each_method_step() # append all the methods include variables in to method_steps_recoder
        self.image_center.update_img(img)r

最终结果

优化部分

这次我们先完成了图片的滑条优化功能,我们把因为滑条的连续变动,
导致图片的连续变化,电脑计算跟不上的问题进行了修正。

实现「还原」或「重做」功能的部分

我们考虑到现有机制如果真的想要实现「还原」或「重做」的功能,
我们必须把「滑条」与「滑条影响图片的内容」介面整个进行修改,

也就是说XD,我们之前设计的介面还不够细、想得不够周全XD

应该是要:

滑条控制(只有一个) -> 
new 一个新的图片变化方法(方法应该要多组,可改多次) ->
保存图片修改方法、UI变化内容(最好可以附带一段此方法的说明,含操作的变数) ->
保存此方法(存进 list)。

最後就是不断循环。

有机会我们再把程序的这部分架购进行优化,目前这个做下去预计又是一个架构上的大改了XD

最终成品!

30天的结语

今天算是铁人赛这三年中最辛苦的一年,
老实说我有先囤了一些稿才来报名,因为今年公司比较忙碌,
而且甚至到铁人赛的最後一天才开赛XD,
但没想到最後居然还是在最後几天被迫熬夜加班才跟得上进度XD。

不过我抱持的心情就是,既然都参加了,就一定要好好的把它完成!
所以才会想先屯稿、拖到最後一天开赛XD

说真的我觉得写铁人赛最大的受惠者永远是作者,
30天前我根本连Qt都没用过,现在我也能变成这样XD
真的信不信由你,我真的是30天内从零开始学的XD,
所以才说如果真的有心想学,铁人赛最终会是让自己受惠最多的短时间高度成长体验。

Reference


★ 本文也同步发於我的个人网站(会有内容目录与显示各个小节,阅读起来更流畅):【PyQt5】Day 30 - final project - 3 / 来搞一个自己的 photoshop 吧!把每个方法封装起来制作出还原功能吧! (结合 PyQt + OpenCV)


<<:  Day 30 - 3D绘图篇 - 噪声地形演算II - 成为Canvas Ninja ~ 理解2D渲染的精髓

>>:  Day30 ATT&CK for ICS - Inhibit Response Function(2)

Day28 - 集成学习 (ensemble learning) part1

集成学习 (ensemble learning) 的概念在於透过结合多个不同的模型来达到不同模型之间...

D20 - 用 Swift 和公开资讯,打造投资理财的 Apps { 移动平均线(MA线)实作.3 }

扩充 MAUtility,让原来的 func 能计算 n 条均线 在原来的 func 上加上 ran...

Day 11 - 用Kotlin解数学题:考拉兹猜想

Day 11 - 用Kotlin解数学题:考拉兹猜想 今天我们会用我前面所教的,来解今天的数学题,顺...

你不想动的卡点在...?

早起运动Day12 - 你不想动的点在哪里 《试错力》这本书,讲的是创新如何从无到有。 试错的重点有...

如何利用 Gsap ScrollTrigger 做出滚动动画? Vue 专案 载入 ScrollTrigger

最近看了很多滚动动画,实在让人惊叹!! 查了背後逻辑发现是用 Gsap 里的 ScrollTrigg...