這一篇我們會繼續拿現有的 day 15 成品來改,
接下來我們要面對關於「處理圖片」與「顯示圖片」不一致的問題。
這是一個會影響非常深遠的問題,因此我們需要早點針對這個問題進行規劃。
我們接下來的討論,會基於讀者已經先讀過我 day5 文章 的架構下去進行程式設計
如果還不清楚我程式設計的邏輯 (UI.py、controller.py、start.py 分別在幹麻)
建議先閱讀 day5 文章後再來閱讀此文。
https://www.wongwonggoods.com/python/pyqt5-5/
https://github.com/howarder3/ironman2021_PyQt5_photoshop/tree/main/day16_mouse_get_pos
為什麼會有「處理圖片」與「顯示圖片」不一致的問題?
最主要的原因是因為我們拿進來的圖片可能會解析度較高,
而我們處理的視窗就那麼大,我們沒辦法每次都讓他已「原解析度」來顯示。
所以在「處理圖片」與「顯示圖片」之間溝通的橋樑我們必須早點做處理。
而在我們程式中,「處理圖片」與「顯示圖片」分別對應到的是以下兩個變數。
依照 day15 的邏輯,我們處理圖片顯示的過程中如下,
我們來看看這其中有沒有什麼可以簡化的地方。
self.img = cv2.imread(self.img_path)
self.qimg = QImage(self.img, self.origin_width, self.origin_height, bytesPerline, QImage.Format_RGB888).rgbSwapped()
self.origin_qpixmap = QPixmap.fromImage(self.qimg)
self.label_img.setPixmap(self.qpixmap)
OpenCV image -> Qimg -> QPixmap -> Qlabel顯示
我們目前最多是在 QPixmap 這裡才處理縮放的問題。
但接下來也許我們會需要針對原圖進行改動,這時候我們會需要處理原解析度的圖片。
「也就是說,雖然我們是在 QPixmap 作業,但實際上處理的層級是在 OpenCV image
」
我們簡化這個流程後,我們可以知道我們可以記錄以下訊息會更方便我們處理:
並且可以得到換算公式:
「QPixmap 現在的長寬」=「OpenCV image 的長寬」*「QPixmap 與 OpenCV image 的比例差距」
雖然上面我們已經把公式都寫出來也整理好了,但我覺得換算上還是很容易混淆...
例如:一不小心可能就會不小心把公式寫錯邊,到底誰乘誰?、到底誰除誰?
所以我們就統一用「正規化」來溝通吧,這樣標準就一定一致了。
這個做法的優點就是直覺,但使用公式上需注意有沒有不小心乘除搞錯。
等等我們要進行座標 (x ,y) 換算時更需要小心。
我們一律先把 (x,y) 座標正規化至一個長寬介於為 0~1 的比例上,
再來進行後續的換算,這樣我們只要知道「顯示圖片」、「實際圖片」的長寬,
在處理上都一慮用正規化的概念下去想 (x, y),
我們會相對比較難犯下不小心搞錯公式的問題。
簡單來說,可以比較不容易出現公式錯誤的問題。(對我個人來說)
我們今天要來取得圖片上的座標,會由 day 15 的結果繼續進行更改,
上述的討論中,我們已經有討論到我們怎麼樣處理「顯示圖片」與「原先圖片」的差異,
我們就直接在 UI 上寫下以下內容,並給予對應參數:
*「點擊座標(顯示圖片的座標)」:label_click_pos
*「換算後,正規化的座標」:label_norm_pos
*「實際座標(對應到原圖片的座標)」:label_real_pos
我設計的介面如同上圖
一樣的編譯指令,我們加上 -x (也可不加),
我們就可以先檢視看看轉換後的視窗是不是跟我們想像的一樣。
pyuic5 -x day16.ui -o UI.py
一樣,這程式只有介面 (視覺上的呈現),沒有任何互動功能
python UI.py
這樣我們的介面就大致出來囉!
這次我們新增了 3 個 label
*「點擊座標(顯示圖片的座標)」:label_click_pos
*「換算後,正規化的座標」:label_norm_pos
*「實際座標(對應到原圖片的座標)」:label_real_pos
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 兩支程式來控制我們的系統,
我們接續 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)
我們替 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
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()
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, y),與正規化後介於 0~1 之間呈現比例展示的 x, y 座標。
並將這些資訊傳入我們修改文字的 function 中。
因為只是純更新資訊,我們將此 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。
這邊有個衍伸的問題,我們在 UI 介面上點擊的原點在哪?
也就是說 (0, 0) 是從哪裡開始算的呢?
我們可以順著我們剛剛做出來的成品,一路找到 (0, 0) 的位置,
我們發現 (0, 0) 座標剛好就位於「圖片」的左上角,
而不是 「UI介面」的左上角,看起來完全這符合我們預期
(這邊只是再確認座標與我們想像無誤,免得後續才回來處理很麻煩)
至於圖片的最右下角,座標又是什麼呢?
我們可以發現就是圖片目前「顯示」的解析度的上限值,
因此我們可以完全確認,我們正在操作的座標就是 QPixmap 的座標,
我們的換算都可以由 QPixmap 出發,依照比例進行換算。
★ 本文也同步發於我的個人網站(會有內容目錄與顯示各個小節,閱讀起來更流暢):【PyQt5】Day 16 - 在 PyQt5 中取得圖片座標 (滑鼠位置) mousePressEvent,觀察圖片在 Qt 中產生的方式,對原圖進行座標換算處理