分類

展開全部 | 收合全部

分類

展開全部 | 收合全部

【PyQt5】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

⭐Python PyQt5 相關文章整理⭐:
⭐基礎知識與架構篇⭐:
1.【PyQt5】Day 1 – 安裝 PyQt,建立自己的第一支 PyQt5 程式
2.【PyQt5】Day 2 – 利用 Qt designer 建立第一支有自己介面的 PyQt5 程式
3.【PyQt5】Day 4 – 重要的 Qt 程式邏輯觀念,務必先有此觀念後面才會懂自己在幹嘛
4.【PyQt5】Day 5 – 開始來設計我們的 controller.py,改以「程式角度」來說明如何建立 PyQt 的系統
⭐基本元件應用篇⭐:
1.【PyQt5】Day 6 – 我們的第一個 output 手段 – Qlabel
2.【PyQt5】Day 7 – 我們的第一個 input 手段 – QPushButton
3.【PyQt5】Day 8 – 我們的第二個 input 手段 – QLineEdit
4.【PyQt5】Day 9 – 以 QLineEdit, QTextEdit, QPlainTextEdit 作為文字的輸入
6.【PyQt5】Day 14 - 使用 QSlider 製作可拖曳的滑條
8.【PyQt5】Day 19 - 使用 QProgressBar,製作進度條的功能
⭐介面系統控制篇⭐:
5.【PyQt5】Day 10 – 以 QFileDialog 讀取系統的檔案、資料夾
7.【PyQt5】Day 18 / Project 使用 QTimer,自製碼表(計時器) PyQt5 stopwatch DIY
1.【PyQt5】Day 20 - PyQt 最重要的 QThread 概念 / 為什麼 windows, mac, ubuntu (linux) 程式會「沒有回應」?
2.【PyQt5】Day 21 – 透過 PyQt 實現滑鼠監聽總整理,完全掌握滑鼠控制 (listen mouse)
3.【PyQt5】Day 22 – PyQt 視窗的個性化/屬性控制 setWindowFlags,禁止放大縮小、永遠顯示於最上層/最下層
4.【PyQt5】Day 23 – 使用系統內建的調色盤 QColorDialog,來替我們選擇顏色 QColor (Color Picker)
5.【PyQt5】Day 24 / Project 偵測滑鼠目前指示顏色的小工具 (滴管工具), 利用 QCursor 偵測滑鼠, QApplication 取得截圖
⭐影像處理篇⭐:
1.【PyQt5】Day 11 – 以 Qlabel 在 PyQt 中顯示圖片 (基於 QImage 使用 OpenCV)
2.【PyQt5】Day 12 – 建立一個可以縮放圖片大小的顯示器 (基於 QImage 使用 OpenCV)
3.【PyQt5】Day 13 – 使用 QVBoxLayout, QscrollArea 製作出捲軸,以高解析度檢視圖片 (基於 QImage 使用 OpenCV)
4.【PyQt5】Day 15 / Project 與檔案功能整合,製作出可讀取圖片並可縮放的 UI 介面 (使用 PyQt + OpenCV)
5.【PyQt5】Day 16 - 在 PyQt5 中取得圖片座標 (滑鼠位置) mousePressEvent,觀察圖片在 Qt 中產生的方式,對原圖進行座標換算處理
6.【PyQt5】Day 17 / Project 製作標註 roi 工具, 開始導入 OpenCV 作為繪圖引擎, 在圖上畫點並顯示座標
⭐project 篇⭐:
1.【PyQt5】Day 25 / Project 自己做一個影片播放器 DIY video player (結合 PyQt + OpenCV)
2.【PyQt5】Day 26 / Project 替我們影片播放器增加一個顯示進度的滑條 video player add slider (與昨日 bottleneck 處理細節)
3.【PyQt5】Day 27 / Project 製作影片 ROI 標註工具 (PyQt 結合 OpenCV 在圖上畫點畫線)
4.【PyQt5】Day 28 / final project – 1 來搞一個自己的 photoshop 吧!UI 篇 + 純程式架構篇 (結合 PyQt + OpenCV)
5.【PyQt5】Day 29 / final project – 2 來搞一個自己的 photoshop 吧!後段程式細節篇 (結合 PyQt + OpenCV)
6.【PyQt5】Day 30 / final project – 3 來搞一個自己的 photoshop 吧!把每個方法封裝起來製作出還原功能吧! (結合 PyQt + OpenCV)
⭐打包程式篇⭐:
1.【PyQt5】Day 3 – 用 pyinstaller 將 python 程式打包,把每天的成果分享給你的親朋好友
⭐【喜歡我的文章嗎? 歡迎幫我按讚~ 讓基金會請創作者喝一杯咖啡!
如果喜歡我的文章,請幫我在下方【按五下Like】 (Google, Facebook 免註冊),會由 「LikeCoin」 贊助作者鼓勵繼續創作,讀者們「只需幫忙按讚,完全不用出錢」哦!

likecoin-steps
Howard Weng
Howard Weng

我是 Howard Weng,很多人叫我嗡嗡。這個網站放了我的各種筆記。希望這些筆記也能順便幫助到有需要的人們!如果文章有幫助到你的話,歡迎幫我點讚哦!
另外,因為定位是「個人的隨手筆記」,有些文章內容「⚠️可能我理解有誤⚠️」或「?只寫到一半?」,如果有發現這樣的情況,歡迎在該文章的最下面留言提醒我!我會儘快修正或補上!感謝大家的建議與幫忙,讓網站能變得更好?

文章: 890

1 則留言

★留個言吧!內容有誤或想要補充也歡迎與我討論!