这一篇我们会继续拿现有的 day 16 成品来改,
我们在 day 16 已经学会了如何取得点座标,
接下来我们要将「点画在画面上」、并「取得该点座标」,
而点座标又可分为「正规化 roi 比例座标」、「实际图片座标」
而在其中,「画点」的功能,我们虽然能够透过 PyQt 的 Qpainter 实现,
不过因为我们後续也会大量使用 OpenCV 作为我们图片处理的引擎,
所以不如就趁先在开始导入吧!
我们接下来的讨论,会基於读者已经先读过我 day5 文章 的架构下去进行程序设计
如果还不清楚我程序设计的逻辑 (UI.py、controller.py、start.py 分别在干麻)
建议先阅读 day5 文章後再来阅读此文。
https://www.wongwonggoods.com/python/pyqt5-5/
https://github.com/howarder3/ironman2021_PyQt5_photoshop/tree/main/day17_roi_drawer
为了往後的开发顺利,这次的开发我们必须谨慎先规划一下了,
接下来我们想导入的 OpenCV 作为我们图像处理的引擎,
我预期会是一个像 library 一样的模组,
我们把所有 OpenCV 图像处理的功能的「细节」做在里面,
而 img_controller.py 只需要呼叫「这个 OpenCV engine 的 API 即可顺利使用
」。
最好的情况甚至是 img_controller.py 都不用「import cv2」,
而只有 OpenCV engine 这支程序统一「import cv2」,
在此处理 OpenCV 相关的事情,所以我们等等也会稍微修改一下 day16 的部分。
我们接续 day 16 的结果进行修改,
我们新增两个 QTextEdit,作为 roi 输出的栏位。
为什麽使用 QTextEdit? 因为这样我们才能复制我们要用的结果XDD
Qlabel 只能显示就不能复制了。
我们就直接在 UI 上新增以下内容,并给予对应参数:
*「显示资讯用,不会改动 - Ratio ROI」:label_info_ratio_roi
*「显示资讯用,不会改动 - Real ROI」:label_info_real_roi
*「依比例表示的 ROI 显示栏位」:text_ratio_roi
*「依实际图片座标表示的 ROI 显示栏位」:text_real_roi
我设计的介面如同上图
一样的编译指令,我们加上 -x (也可不加),
我们就可以先检视看看转换後的视窗是不是跟我们想像的一样。
pyuic5 -x day17.ui -o UI.py
一样,这程序只有介面 (视觉上的呈现),没有任何互动功能
python UI.py
这样我们的介面就大致出来罗!
这次我们新增了 3 个 label
*「显示资讯用,不会改动 - Ratio ROI」:label_info_ratio_roi
*「显示资讯用,不会改动 - Real ROI」:label_info_real_roi
*「依比例表示的 ROI 显示栏位」:text_ratio_roi
*「依实际图片座标表示的 ROI 显示栏位」:text_real_roi
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)
我们今天会新增一个 opencv_engine.py,作为图像处理的引擎使用,
对我来说最理想的情况,就是只有 opencv_engine.py 这支程序 import cv2,
然後我们将所有会用到 OpenCV 的功能封装成 API (function),
再给 img_controller.py 去 call API(function)。
为了使用方便,我们把这个程序会呼叫的部分全部都用 class 封装起来,
而且方便直接取用,我们让使用者不需要先宣告一个 instance(物件),
全部都写成 static method,方便使用者直接呼叫这些已经包好的功能。
import cv2
class opencv_engine(object):
@staticmethod
def point_float_to_int(point):
return (int(point[0]), int(point[1]))
@staticmethod
def read_image(file_path):
return cv2.imread(file_path)
@staticmethod
def draw_point(img, point=(0, 0), color = (0, 0, 255)): # red
point = opencv_engine.point_float_to_int(point)
print(f"get {point=}")
point_size = 1
thickness = 4
return cv2.circle(img, point, point_size, color, thickness)
我们一共封装了三个功能,point_float_to_int, read_image, draw_point,
对,所以我们等等也会把前几天的透过 cv2.imread 的功能搬到这里来,
再改用 staticmethod 的呼叫方式 opencv_engine.read_image(file_path),
取得回传的图片。
因为 OpenCV 的点座标处理会常常要求一定要整数输入,
而我们又常常以 (x, y) 这样的座标为单位进行处理,
而不是个别传入 x 与 y,因此乾脆独立依格 function 专门把传进来的 (x, y)
强转成 (int x, int y) 并回传。
这个就是我们的 OpenCV 老朋友了,
为了使程序更加弹性,我特别把 point, color 拉出来,
因此之後传入时,我们除了座标之外、也可以指定传入的颜色。
不知道怎麽使用 OpenCV 画点?
可以参考我的另外一篇文,内有详细说明:【OpenCV】11 – OpenCV 建立新空白图、画点、画圆 create new pictures, draw points and draw circle
依照上面的内容我们需要「修改图片传入的部分」,
另外也需要新增「显示 roi 文字的部分」、「呼叫显示点的功能」
我们要从 opencv_engine.py 导入 class opencv_engine,
因此要 from opencv_engine import opencv_engine
(前者为 .py 档名,後者为 class 名称)
from opencv_engine import opencv_engine
这边只截录重点部分,
因为我们这只 img_controller.py 程序不想再 import cv2 了,
全交由 opencv_engine 处理,
所以我们将原来的 cv2.imread() 改为 opencv_engine.read_image()
def read_file_and_init(self):
try:
self.origin_img = opencv_engine.read_image(self.img_path)
self.origin_height, self.origin_width, self.origin_channel = self.origin_img.shape
except:
self.origin_img = opencv_engine.read_image('sad.png')
self.origin_height, self.origin_width, self.origin_channel = self.origin_img.shape
我们刚刚已经封装好程序的功能了,所以这边可以直接呼叫 opencv_engine.draw_point(),
并把点座标传入,记得先换算好座标 (昨天提到的,使用座标正规化统一处理)
def draw_point(self, point):
# give me normalized point, i will help you to transform to origin cv image position
cv_image_x = point[0]*self.origin_width
cv_image_y = point[1]*self.origin_height
self.display_img = opencv_engine.draw_point(self.display_img, (cv_image_x, cv_image_y))
self.__update_img()
def __update_text_point_roi(self, point):
# give me normalized point, i will help you to transform to origin cv image position
cv_image_x = point[0]*self.origin_width
cv_image_y = point[1]*self.origin_height
self.ui.text_ratio_roi.append(f"[{point[0]:.6f}, {point[1]:.6f}]")
self.ui.text_real_roi.append(f"[{int(cv_image_x)}, {int(cv_image_y)}]")
这边就单纯更新文字了,有正规化的 roi 点击座标,
相信更新起来也相当容易吧,只需要做一些简单的运算。
这边为了我自己使用 roi 要使用的格式,我特别改为 list 显示点座标,
没有什麽特别的不使用 tuple 而使用 list 的原因。
新增最後两行,画点 draw_point、更新 roi 文字资讯 __update_text_point_roi
def get_clicked_position(self, event):
x = event.pos().x()
y = event.pos().y()
self.__update_text_clicked_position(x, y)
norm_x = x/self.qpixmap.width()
norm_y = y/self.qpixmap.height()
self.draw_point((norm_x, norm_y))
self.__update_text_point_roi((norm_x, norm_y))
为什麽会突然提到这个...,
这个就是我前几天不小心欠下的技术债,
因为我没有先规划设定比例的逻辑,
导致现在更新图片的思路非常混乱。
什麽混乱法呢? 就是显示图片的时候可能需要先想,要不要先去 call 设定比例,
欸不对设定比例後接续也会更新图片。
可是我有时候只想更新图片,比例也没变,我还需要 call 设定比例吗???
就是没有好好规划啦! 所以我决定重新规划架构,并画出来这样思路就很清楚了。
这边我已经先优化了 ratio 那边的混乱逻辑才画出来的,
所以这已经是整理过的一版XD,
啊不过做为示范,这里其实还有可以优地的地方,
观察下图我们可以发现:
在初始化事件中 init() 与 set_path() 设定新图片路径中,
我们有同样的逻辑,都是先呼叫读档後去更新图片。
这些都是要整理出流程图才知道能优化的,没整理前我也没想到这里可以优化。
我们把重复的部分都塞进读档内,让读档後可以直接更新,
流程图的逻辑就可以看起来更乾净。
这个也是为了以後的自己好,因为程序会越写越大,
趁现在我们把命名格式统一一下,
好啦(累瘫,至少现在趁程序还小,赶快把该整理的格式都整理後,之後相信会舒服很多的。
这会是我最後一次贴完整程序码了,因为越写越大XDD,
再贴完整程序码於文章中会太占版面XDD,
之後会只贴更新的部分,想要完整程序码的话可以去看我的 github。
from PyQt5 import QtCore
from PyQt5.QtGui import QImage, QPixmap
from opencv_engine import opencv_engine
class img_controller(object):
def __init__(self, img_path, ui):
self.img_path = img_path
self.ui = ui
self.ratio_value = 50
self.read_file_and_init()
def read_file_and_init(self):
try:
self.origin_img = opencv_engine.read_image(self.img_path)
self.origin_height, self.origin_width, self.origin_channel = self.origin_img.shape
except:
self.origin_img = opencv_engine.read_image('sad.png')
self.origin_height, self.origin_width, self.origin_channel = self.origin_img.shape
self.display_img = self.origin_img
self.__update_text_file_path()
self.ratio_value = 50 # re-init
self.__update_img()
def set_path(self, img_path):
self.img_path = img_path
self.read_file_and_init()
def __update_img_ratio(self):
self.ratio_rate = pow(10, (self.ratio_value - 50)/50)
qpixmap_height = self.origin_height * self.ratio_rate
self.qpixmap = self.qpixmap.scaledToHeight(qpixmap_height)
self.__update_text_ratio()
self.__update_text_img_shape()
def __update_img(self):
bytesPerline = 3 * self.origin_width
qimg = QImage(self.display_img, self.origin_width, self.origin_height, bytesPerline, QImage.Format_RGB888).rgbSwapped()
self.qpixmap = QPixmap.fromImage(qimg)
self.__update_img_ratio()
self.ui.label_img.setPixmap(self.qpixmap)
self.ui.label_img.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop)
self.ui.label_img.mousePressEvent = self.set_clicked_position
def __update_text_file_path(self):
self.ui.label_file_name.setText(f"File path = {self.img_path}")
def __update_text_ratio(self):
self.ui.label_ratio.setText(f"{int(100*self.ratio_rate)} %")
def __update_text_img_shape(self):
current_text = f"Current img shape = ({self.qpixmap.width()}, {self.qpixmap.height()})"
origin_text = f"Origin img shape = ({self.origin_width}, {self.origin_height})"
self.ui.label_img_shape.setText(current_text+"\t"+origin_text)
def __update_text_clicked_position(self, x, y):
# give me qpixmap point
self.ui.label_click_pos.setText(f"Clicked postion = ({x}, {y})")
norm_x = x/self.qpixmap.width()
norm_y = y/self.qpixmap.height()
print(f"(x, y) = ({x}, {y}), normalized (x, y) = ({norm_x}, {norm_y})")
self.ui.label_norm_pos.setText(f"Normalized postion = ({norm_x:.3f}, {norm_y:.3f})")
self.ui.label_real_pos.setText(f"Real postion = ({int(norm_x*self.origin_width)}, {int(norm_y*self.origin_height)})")
def set_zoom_in(self):
self.ratio_value = max(0, self.ratio_value - 1)
self.__update_img()
def set_zoom_out(self):
self.ratio_value = min(100, self.ratio_value + 1)
self.__update_img()
def set_slider_value(self, value):
self.ratio_value = value
self.__update_img()
def set_clicked_position(self, event):
x = event.pos().x()
y = event.pos().y()
self.__update_text_clicked_position(x, y)
norm_x = x/self.qpixmap.width()
norm_y = y/self.qpixmap.height()
self.draw_point((norm_x, norm_y))
self.__update_text_point_roi((norm_x, norm_y))
def draw_point(self, point):
# give me normalized point, i will help you to transform to origin cv image position
cv_image_x = point[0]*self.origin_width
cv_image_y = point[1]*self.origin_height
self.display_img = opencv_engine.draw_point(self.display_img, (cv_image_x, cv_image_y))
self.__update_img()
def __update_text_point_roi(self, point):
# give me normalized point, i will help you to transform to origin cv image position
cv_image_x = point[0]*self.origin_width
cv_image_y = point[1]*self.origin_height
self.ui.text_ratio_roi.append(f"[{point[0]:.6f}, {point[1]:.6f}]")
self.ui.text_real_roi.append(f"[{int(cv_image_x)}, {int(cv_image_y)}]")
照我们 day5 的程序架构,我们执行
python start.py
我们所有在画面点击的点,都会在下方以两种不同的方式表示,
分别是比例的座标、实际的图片座标,
就这样完成了我要使用的 roi 标注工具。
★ 本文也同步发於我的个人网站(会有内容目录与显示各个小节,阅读起来更流畅):【PyQt5】Day 17 / Project 制作标注 roi 工具, 开始导入 OpenCV 作为绘图引擎, 在图上画点并显示座标
fetch 改善了 XHR 又长又麻烦的写法,简化了程序码使阅读容易许多,而 fetch retur...
在昨天我们简单介绍了框架是如何产生配对後,今天我们要来部署 Open-Match 所需要的环境与核心...
我们的系统架构很单纯,分为托管在 Vercel 上的 Next.js 前端,以及托管在 BlueHo...
不管在哪个阶段,在写程序时总是会遇到大大小小的问题,不是程序不照着你的想法走,就是他连动都不想动。在...
Object storage - 云端最流行的储存方式 Object storage和file st...