这一篇我们会继续拿现有的 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 中产生的方式,对原图进行座标换算处理
<<: Material UI in React [ Day 30 ] 总结
PostgreSQL last N in-table cache 探讨 前些天对悠游卡储值时,加值机...
在初学程序的时候,一定很讨厌例外(Exception)发生,因为程序就没办法跑完了,也代表我们可能有...
今天我们要实作 Signaling server 的部分: 建立文件 # 进入要放专案的路径 ❯ c...
前言 昨天我们设计好UI介面後, 我们有一堆按钮和文字框的"元件", 要让这些元...
-资讯安全 安全是指保护某物免受危险或威胁的过程和达到的状态。 资讯安全是一门通过安全控制保护资讯...