➣ Reading Time: 17 minutes

看完這篇文章你會得到的成果圖

前言

這一篇我們會繼續拿現有的 day 15 成品來改,
接下來我們要面對關於「處理圖片」與「顯示圖片」不一致的問題。

這是一個會影響非常深遠的問題,因此我們需要早點針對這個問題進行規劃。

我們接下來的討論,會基於讀者已經先讀過我 day5 文章 的架構下去進行程式設計
如果還不清楚我程式設計的邏輯 (UI.py、controller.py、start.py 分別在幹麻)
建議先閱讀 day5 文章後再來閱讀此文。

此篇文章的範例程式碼 github

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

我們先來分析「處理圖片」與「顯示圖片」不一致的問題

為什麼會有「處理圖片」與「顯示圖片」不一致的問題?
最主要的原因是因為我們拿進來的圖片可能會解析度較高,

而我們處理的視窗就那麼大,我們沒辦法每次都讓他已「原解析度」來顯示。
所以在「處理圖片」與「顯示圖片」之間溝通的橋樑我們必須早點做處理。

而在我們程式中,「處理圖片」與「顯示圖片」分別對應到的是以下兩個變數。

  • 顯示的圖片 self.qpixmap
  • 處理中的圖片 self.img

分析兩者之間的「程式」關係

依照 day15 的邏輯,我們處理圖片顯示的過程中如下,
我們來看看這其中有沒有什麼可以簡化的地方。

1. self.img 是由 OpenCV 的 imread 取得的圖片 (讀入的原圖)

self.img = cv2.imread(self.img_path)

2. 我們會先經由以下處理,將他轉為 Qimg

self.qimg = QImage(self.img, self.origin_width, self.origin_height, bytesPerline, QImage.Format_RGB888).rgbSwapped()

3. 再來會由 Qimg 轉為 QPixmap

self.origin_qpixmap = QPixmap.fromImage(self.qimg)

4. Qpixmap 可能會經由一些縮放的處理,最後藉由 Qlabel 顯示在畫面上

self.label_img.setPixmap(self.qpixmap)

分析這個流程,發現實際上圖片經過了很多次的轉換,才到最後顯示的部分。

OpenCV image -> Qimg -> QPixmap -> Qlabel顯示

我們目前最多是在 QPixmap 這裡才處理縮放的問題。
但接下來也許我們會需要針對原圖進行改動,這時候我們會需要處理原解析度的圖片。

也就是說,雖然我們是在 QPixmap 作業,但實際上處理的層級是在 OpenCV image

我們簡化這個流程後,我們可以知道我們可以記錄以下訊息會更方便我們處理:

  • QPixmap 現在的長寬 (會因為顯示而改變)
  • QPixmap 與 OpenCV image 的比例差距 (會因為顯示而改變)
  • OpenCV image 原圖的長寬 (永遠不變)

並且可以得到換算公式:

「QPixmap 現在的長寬」=「OpenCV image 的長寬」*「QPixmap 與 OpenCV image 的比例差距」

有沒有更不容易混淆的做法? – 不如我們都「正規化」一下

雖然上面我們已經把公式都寫出來也整理好了,但我覺得換算上還是很容易混淆…
例如:一不小心可能就會不小心把公式寫錯邊,到底誰乘誰?、到底誰除誰?

所以我們就統一用「正規化」來溝通吧,這樣標準就一定一致了。

  • 如下圖:我們原來的作法

這個做法的優點就是直覺,但使用公式上需注意有沒有不小心乘除搞錯。
等等我們要進行座標 (x ,y) 換算時更需要小心。

  • 如下圖:我們優化的作法 (正規化)

我們一律先把 (x,y) 座標正規化至一個長寬介於為 0~1 的比例上,
再來進行後續的換算,這樣我們只要知道「顯示圖片」、「實際圖片」的長寬,
在處理上都一慮用正規化的概念下去想 (x, y),
我們會相對比較難犯下不小心搞錯公式的問題。

簡單來說,可以比較不容易出現公式錯誤的問題。(對我個人來說)

UI 設計部份 (UI.py)

我們今天要來取得圖片上的座標,會由 day 15 的結果繼續進行更改,
上述的討論中,我們已經有討論到我們怎麼樣處理「顯示圖片」與「原先圖片」的差異,

我們就直接在 UI 上寫下以下內容,並給予對應參數:

*「點擊座標(顯示圖片的座標)」:label_click_pos
*「換算後,正規化的座標」:label_norm_pos
*「實際座標(對應到原圖片的座標)」:label_real_pos

我設計的介面如同上圖

轉換成 UI.py

一樣的編譯指令,我們加上 -x (也可不加),
我們就可以先檢視看看轉換後的視窗是不是跟我們想像的一樣。

轉換 day16.ui -> UI.py

pyuic5 -x day16.ui -o UI.py

執行看看 UI.py 畫面是否如同我們想像

一樣,這程式只有介面 (視覺上的呈現),沒有任何互動功能

  • 看看我們製作出來的介面
python UI.py

這樣我們的介面就大致出來囉!

controller 設計部份 (controller.py)

從 UI.py 中找出物件名稱

這次我們新增了 3 個 label

*「點擊座標(顯示圖片的座標)」:label_click_pos
*「換算後,正規化的座標」:label_norm_pos
*「實際座標(對應到原圖片的座標)」:label_real_pos

同 day13 的 scrollArea 說明,我們一樣需要刪除 scrollAreaWidgetContents 的部份

  • 新增與調整的 scrollArea 片段
self.scrollArea = QtWidgets.QScrollArea(self.verticalLayoutWidget)
self.scrollArea.setWidgetResizable(True)
self.scrollArea.setObjectName("scrollArea")
# self.scrollAreaWidgetContents = QtWidgets.QWidget()
# self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 937, 527))
# self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
# self.label_img = QtWidgets.QLabel(self.scrollAreaWidgetContents)
self.label_img = QtWidgets.QLabel() # 調整為只單純宣告
self.label_img.setGeometry(QtCore.QRect(0, 0, 941, 521))
self.label_img.setObjectName("label_img")
# self.scrollArea.setWidget(self.scrollAreaWidgetContents)
self.scrollArea.setWidget(self.label_img)

取得名稱後,去修改控制部分

截至到 day15,總共有 controller.py, img_controller.py 兩支程式來控制我們的系統,

  • controller.py:主要控制程式的部分
  • img_controller.py:另外封裝專門處理圖片的部分

修改控制主程式的 controller.py

我們接續 day 15 的內容,
新增我們剛剛在 UI 增加的 label,因為也是跟圖片有關的內容,
我們只做參數的傳遞,其他交由 img_controller.py 處理。

self.img_controller = img_controller(img_path=self.file_path,
                                     label_img=self.ui.label_img,
                                     label_file_path=self.ui.label_file_name,
                                     label_ratio=self.ui.label_ratio,
                                     label_img_shape=self.ui.label_img_shape,
                                     label_click_pos=self.ui.label_click_pos,
                                     label_norm_pos=self.ui.label_norm_pos,
                                     label_real_pos=self.ui.label_real_pos)

另外封裝專門處理圖片的 img_controller.py

我們替 day 15 的 function 「擴充」新的偵測座標功能

宣告的地方,新增傳入的參數

class img_controller(object):
    def __init__(self, img_path, label_img, label_file_path, label_ratio, label_img_shape, label_click_pos, label_norm_pos, label_real_pos):
        self.label_click_pos = label_click_pos
        self.label_norm_pos = label_norm_pos
        self.label_real_pos = label_real_pos

更新圖片時,同步增加監聽偵測滑鼠位置的 mousePressEvent

def __update_img(self):       
        self.label_img.setPixmap(self.qpixmap)
        self.label_img.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)
        self.label_img.mousePressEvent = self.get_clicked_position

self.label_img.mousePressEvent = self.get_clicked_position

我們替 Qlabel 增加一個 mousePressEvent,而宣告的 function 就是我們等等會撰寫的 get_clicked_position()

幫助我們取得回傳座標的 get_clicked_position

def get_clicked_position(self, event):
    x = event.pos().x()
    y = event.pos().y() 
    self.norm_x = x/self.qpixmap.width()
    self.norm_y = y/self.qpixmap.height()
    print(f"(x, y) = ({x}, {y}), normalized (x, y) = ({self.norm_x}, {self.norm_y})")
    self.__update_text_clicked_position(x, y)

我們每觸發一次上述的點擊 mousePressEvent,就會執行一次 get_clicked_position 的內容,
我們可以從 event 這個變數取得點擊的 (x, y)

  • x = event.pos().x()
  • y = event.pos().y()

在我們最上方的討論中,我們決定要把所有的座標進行正規化,
以避免直接運算,容易產生的公式乘除錯誤的問題,
因此我們直接透過以下公式將座標正規化。

  • self.norm_x = x/self.qpixmap.width()
  • self.norm_y = y/self.qpixmap.height()

最後我們可以顯示一下,我們所點擊的 (x, y),與正規化後介於 0~1 之間呈現比例展示的 x, y 座標。
並將這些資訊傳入我們修改文字的 function 中。

  • print(f”(x, y) = ({x}, {y}), normalized (x, y) = ({self.norm_x}, {self.norm_y})”)
  • self.__update_text_clicked_position(x, y)

更新畫面座標資訊的 __update_text_clicked_position()

因為只是純更新資訊,我們將此 function 設為 private,不讓我們能夠輕易存取內容,
我們更新三種座標的顯示:

*「點擊座標(顯示圖片的座標)」:label_click_pos
*「換算後,正規化的座標」:label_norm_pos
*「實際座標(對應到原圖片的座標)」:label_real_pos

def __update_text_clicked_position(self, x, y):
    self.label_click_pos.setText(f"Clicked postion = ({x}, {y})")
    self.label_norm_pos.setText(f"Normalized postion = ({self.norm_x:.3f}, {self.norm_y:.3f})")
    self.label_real_pos.setText(f"Real postion = ({int(self.norm_x*self.origin_width)}, {int(self.norm_y*self.origin_height)})")

這樣就更新完了。

執行結果

照我們 day5 的程式架構,我們執行

python start.py

我們點擊任意的點,就會顯示「該座標」、「正規化座標」、「對應原圖實際座標」。

而在我們的 terminal 當中也會顯示一些我們剛剛印出來的資訊,方便我們 debug。

觀察並檢查座標 (x, y) – 我們在 UI 介面上點擊的原點在哪?

這邊有個衍伸的問題,我們在 UI 介面上點擊的原點在哪?
也就是說 (0, 0) 是從哪裡開始算的呢?

我們可以順著我們剛剛做出來的成品,一路找到 (0, 0) 的位置,

我們發現 (0, 0) 座標剛好就位於「圖片」的左上角,
而不是 「UI介面」的左上角,看起來完全這符合我們預期
(這邊只是再確認座標與我們想像無誤,免得後續才回來處理很麻煩)

至於圖片的最右下角,座標又是什麼呢?

我們可以發現就是圖片目前「顯示」的解析度的上限值,
因此我們可以完全確認,我們正在操作的座標就是 QPixmap 的座標,
我們的換算都可以由 QPixmap 出發,依照比例進行換算。

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