iT邦幫忙

2021 iThome 鐵人賽

DAY 16
0
Software Development

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

【沒錢買ps,PyQt自己寫】Day 16 - 在 PyQt5 中取得圖片座標 (滑鼠位置) mousePressEvent,觀察圖片在 Qt 中產生的方式,對原圖進行座標換算處理

  • 分享至 

  • xImage
  •  

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

前言

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

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

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

https://www.wongwonggoods.com/python/pyqt5-5/

此篇文章的範例程式碼 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 出發,依照比例進行換算。


★ 本文也同步發於我的個人網站(會有內容目錄與顯示各個小節,閱讀起來更流暢):【PyQt5】Day 16 - 在 PyQt5 中取得圖片座標 (滑鼠位置) mousePressEvent,觀察圖片在 Qt 中產生的方式,對原圖進行座標換算處理


上一篇
【沒錢買ps,PyQt自己寫】Day 15 / Project 與檔案功能整合,製作出可讀取圖片並可縮放的 UI 介面 (使用 PyQt + OpenCV)
下一篇
【沒錢買ps,PyQt自己寫】Day 17 / Project 製作標註 roi 工具, 開始導入 OpenCV 作為繪圖引擎, 在圖上畫點並顯示座標
系列文
【今年還是不夠錢買psQQ,不如我們用PyQt自己寫一個】30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言