iT邦幫忙

2021 iThome 鐵人賽

DAY 30
1
Software Development

【今年還是不夠錢買psQQ,不如我們用PyQt自己寫一個】系列 第 30

【沒錢買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)


上一篇
【沒錢買ps,PyQt自己寫】Day 29 - final project - 2 / 來搞一個自己的 photoshop 吧!後段程式細節篇 (結合 PyQt + OpenCV)
系列文
【今年還是不夠錢買psQQ,不如我們用PyQt自己寫一個】30

尚未有邦友留言

立即登入留言