PyQt5提取圆形、矩形及任意多边形ROI区域灰度值并计算ROI区域内像素点灰度值的均值及区域内像素点数
项目需求是实现标题所述功能,并将其嵌入到某行业开源软件中,做为自有产品的配套软件提供给客户,铺垫完毕,上干货。
1、为何选择python?因为开源软件是写的,预留有开放的扩展接口,单功能实现后可以无缝嵌入。
2、为何python Qt?因为之前简单接触过Qt,做界面比较傻瓜。
3、用到的python库(罗列一下,也可以看代码):
PyQt5----界面交互相关
pyqtgraph---ROI提取相关,仅矩形ROI完全使用了该库,圆形ROI基于库进行了细微修改,研究了几天没搞明白参数传递,就给它硬改了一下,任意多边形仅显示使用了库,提取灰度值完全自己撸(借鉴了大佬的代码,均有引用说明)
PIL(高版本实际安装 pip install pillow)
numpy
——————————————————————————————————————————————————————————————
主文件:
import sys from PyQt5.QtWidgets import * #导入PyQt相关模块 from PyQt5.QtGui import * from PyQt5.QtCore import * #from pyqtgraph.graphicsItems import * import pyqtgraph as pg import math from gui import * #导入窗口 import numpy as np from PIL import Image #pillow global dir_str global scene,scene_buf,scene_buf2,vb global item global pic global grayimg#Gray picture global imgarray #class Mywindow(QMainWindow,Ui_Form): #使用这个的话界面布局会乱 class roiwidge(QWidget,Ui_Form):#界面布局自动缩放 def __init__(self,parent=None): super(roiwidge,self).__init__(parent) self.setupUi(self) self.setWindowTitle("ROI") self.graphicsView.mouseReleaseEvent = self.gpmouseReleaseEvent self.graphicsView.mousePressEvent = self.gpmousePressEvent self.graphicsView.mouseDoubleClickEvent = self.gpmouseDoubleClickEvent self.loadButton.clicked.connect(self.load_click) self.loadButton.setText("Select File") self.ClearROIButton.clicked.connect(self.ClearAllROI)#清除界面所有ROI #self.SetROIcolor.clicked.connect(self.SetRoiColorclick) self.RectROI.setChecked(True) self.lastPoint = QPoint() self.endPoint = QPoint() self.PenColor = QColor("red").name() self.points = list()#仅多边形使用 self.name = list() self.rois = list() self.MultiRoiDone = False self.multiroi_count = 0 def load_click(self):#选择图像文件路径 global dir_str global scene global scene_buf global scene_buf2 global vb global item global pic global grayimg#Gray picture global imgarray #self.listWidget.clear()#清空界面缓存 self.tableWidget.clear() self.tableWidget.setHorizontalHeaderLabels(['ROI','Averange'])#设置表头文字 # print(self.tableWidget.rowCount()) # self.tableWidget.setRowCount(1) # self.tableWidget.setItem(0,0,QTableWidgetItem("测试1")) # self.tableWidget.setItem(0,1,QTableWidgetItem("测试2")) #self.filelocation.setText("测试界面") #self指向自身,"Open File"为文件名,"./"为当前路径,最后为文件类型筛选器 fname,ftype = QFileDialog.getOpenFileName(self, "Open File", "C:", "tiff(*.tiff);;png(*.png);;jpg(*.jpg)")#如果添加一个内容则需要加两个分号 # 该方法返回一个tuple,里面有两个内容,第一个是路径, 第二个是要打开文件的类型,所以用两个变量去接受 # 如果用户主动关闭文件对话框,则返回值为空 if fname[0]: # 判断路径非空 dir_str = fname buf = fname buf = buf.replace("\\", "/") self.setWindowTitle("ROI--" + buf) #show picture #pic = QtGui.QPixmap(dir_str) grayimg = Image.open(dir_str).convert('L') #grayimg.show() imgarray = np.asarray(grayimg) size_x = len(imgarray[0]) size_y = len(imgarray) qimg = QImage(dir_str) pic = QPixmap.fromImage(qimg) #img.show() item = QGraphicsPixmapItem(pic) scene = QGraphicsScene(0,0,size_x,size_y) scene_buf2 = 0 scene_buf = pg.ImageItem() scene_buf.setImage(imgarray) vb = pg.ViewBox() vb.addItem(scene_buf) #scene_buf = QGraphicsScene(0,0,size_x,size_y) scene.addItem(item) #scene_buf.addItem(item) self.graphicsView.setScene(scene) #鼠标滚轮 def wheelEvent(self, event): self.scaleView(math.pow(2.0,event.angleDelta().y() / 240.0)) pass #图片缩放 def scaleView(self,scaleFactor): factor = self.graphicsView.transform().scale(scaleFactor,scaleFactor).mapRect(QRectF(0,0,1,1)).width() if (factor < 0.07 or factor > 100 ): return self.graphicsView.scale(scaleFactor,scaleFactor) #鼠标事件 def gpmousePressEvent(self,event): if event.buttons() == QtCore.Qt.LeftButton: self.MultiRoiDone = False point = self.graphicsView.mapToScene(event.pos()) if self.MultiRectROI.isChecked() == True: self.points.append(point) self.test(points=self.points) else: self.lastPoint = point elif event.buttons() == QtCore.Qt.RightButton: self.MultiRoiDone = True self.test(points=self.points) #self.RectROI.setChecked(True) event.accept() def gpmouseReleaseEvent(self,event): #if event.buttons() == QtCore.Qt.LeftButton: print("release left") point = self.graphicsView.mapToScene(event.pos()) self.endPoint = point (x,y,w,h) = self.caculat_Rec(self.lastPoint,self.endPoint) # try: # self.test((x,y),(w,h)) # except Exception as e: # print("捕获异常",e) # finally: # pass self.test((x,y),(w,h)) def gpmouseDoubleClickEvent(self,event): pass #计算矩形ROI的原点和长宽 def caculat_Rec(self,pos1,pos2): x1 = pos1.x() x2 = pos2.x() y1 = pos1.y() y2 = pos2.y() print(x1,x2,y1,y2) if x1 >= x2: x = x2 else : x = x1 if y1 >= y2: y = y2 else : y = y1 h = abs(y1-y2) w = abs(x1-x2) return x,y,w,h #ROI def test(self,pos=(0,0),size=(0,0),points=list()): global frame global scene global scene_buf global scene_buf2 global vb if self.RectROI.isChecked() == True: frame = pg.RectROI(pos,size) self.name.append("roi{}".format(len(self.name)+1)) frame.addTranslateHandle((0,0),name=self.name[-1],index=len(self.name)-1) scene.addItem(frame) #print(pos,size) self.rois.append(frame) buf = imgarray[int(pos[1]):int(pos[1]+size[1]),int(pos[0]):int(pos[0]+size[0])] self.Caculat_Info(buf) elif self.CircleROI.isChecked() == True: #frame = pg.CircleROI(pos,size) #EllipseROI frame = pg.EllipseROI(pos,size) self.name.append("roi{}".format(len(self.name)+1)) frame.addTranslateHandle((0,0),name=self.name[-1],index=len(self.name)-1) scene.addItem(frame) self.rois.append(frame) vb.addItem(frame) buf,mask = frame.getArrayRegion(imgarray,scene_buf,axes=(1, 0))#此处修改了库函数2022年4月29日 vb.removeItem(frame) scene.addItem(frame) self.Caculat_CircleInfo(buf,mask) elif self.MultiRectROI.isChecked() == True: if self.MultiRoiDone: pos = self.points[0] frame = pg.PolyLineROI([]) #frame = pg.PolyLineROI(points).setPoints(points,closed=True) frame.setPoints(points,closed=True) self.name.append("roi{}".format(len(self.name)+1)) frame.addTranslateHandle((0,0),name=self.name[-1],index=len(self.name)-1) self.rois.append(frame) #获取ROI区域 mask,buf = self.Get_MultiROIMask(imgarray,points) # buf1 = np.transpose(buf) # mask1 = np.transpose(mask) # np.savetxt("buf",buf,fmt='%.1f') # np.savetxt("mask1",mask,fmt='%.1f') self.multiroi_count = 0 scene.addItem(frame) mask1 = np.transpose(mask) self.Caculat_CircleInfo(buf,mask1) self.points.clear() else: if self.multiroi_count == 0 : scene_buf2 = len(scene.items()) self.multiroi_count = self.multiroi_count + 1 frame = pg.PolyLineROI(points) scene.addItem(frame) #Set ROI color # def SetRoiColorclick(self): # self.PenColor = QColorDialog.getColor().name() def Caculat_Info(self,piclist): sum = 0 count = 0 avg = 0 for xlist in piclist: for ylist in xlist: sum = sum + ylist count = count + 1 avg = sum /count self.Add_tableWidget(avg) #self.listWidget.addItem(self.name[-1]+"average is:"+str(avg)) #self.tableWidget.items().append(avg) def ClearAllROI(self): global scene # self.listWidget.clear()#清空界面缓存 self.tableWidget.clear() self.tableWidget.setHorizontalHeaderLabels(['ROI','Averange'])#设置表头文字 self.tableWidget.setRowCount(0) self.rois.clear() self.points.clear() self.name.clear() self.rois.clear() #del scene.items()[0:-1] for i in range(len(scene.items())-1): scene.removeItem(scene.items()[0]) print(scene.items()) def Caculat_CircleInfo(self,data,mask): mask1 = np.transpose(mask) sum = 0 count = 0 avg = 0 for i in range(len(mask1)-1): for j in range(len(mask1[0])-1): if mask1[i,j]: sum = sum + data[i,j] count = count + 1 avg = sum / count self.Add_tableWidget(avg) #self.listWidget.addItem(self.name[-1]+"average is:"+str(avg)) #判断点在多边形范围内,其中pt为点,poly为多边形区域 #版权声明:本文为CSDN博主「可可爱爱的小肥肥」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 #原文链接:https://blog.csdn.net/ftmsz12345678/article/details/115375461 def isInsidePolygon(self,pt, poly): c = False i = -1 l = len(poly) j = l - 1 while i < l - 1: i += 1 if((poly[i][0]<=pt[0] and pt[0] < poly[j][0])or( poly[j][0]<=pt[0] and pt[0]<poly[i][0] )): if(pt[1]<(poly[j][1]-poly[i][1]) * (pt[0]-poly[i][0])/( poly[j][0]-poly[i][0])+poly[i][1]): c = not c j=i return c def Get_MultiROIMask(self,picbuf,points):#picbuf传入整幅图像二维数组,points传入原始坐标列表 max_x = 0 max_y = 0 picbuf1 = np.transpose(picbuf) min_x = len(picbuf1)#行数量 min_y = len(picbuf1[0])#列数量 point_list = np.zeros((len(points),2)) for i in range(len(points)): point_list[i][0] = points[i].x() point_list[i][1] = points[i].y() if points[i].x() >= max_x: max_x = points[i].x() if points[i].y() >= max_y: max_y = points[i].y() if points[i].x() <= min_x: min_x = points[i].x() if points[i].y() <= min_y: min_y = points[i].y() #截取多边形所在矩形区域像素值并进行坐标平移 minx_t = int(min_x) miny_t = int(min_y) maxx_t = int(max_x) maxy_t = int(max_y) picbuf1 = picbuf1[minx_t:(maxx_t+1),miny_t:(maxy_t+1)] for i in range(len(points)): point_list[i][0] = point_list[i][0] - minx_t point_list[i][1] = point_list[i][1] - miny_t mask_buf = np.zeros(((maxx_t-minx_t),(maxy_t-miny_t)))#根据多边形所在矩形区域大小创建全0mask,减少计算量 for x in range(len(picbuf1)): for y in range(len(picbuf1[0])): if self.isInsidePolygon((x,y),point_list): mask_buf[x][y] = 1 return (mask_buf,picbuf1) def Add_tableWidget(self,avg): a = self.tableWidget.rowCount() self.tableWidget.setRowCount(a+1) print(self.name[-1]) print('%.2f' % avg) print(self.tableWidget.rowCount()) self.tableWidget.setItem(a,0,QTableWidgetItem(str(self.name[-1]))) self.tableWidget.setItem(a,1,QTableWidgetItem(str('%.2f' % avg))) if __name__=='__main__': app = QApplication(sys.argv) myWin = roiwidge() myWin.show() sys.exit(app.exec_())
界面文件(Qt的*.ui文件直接生成的)
# -*- coding: utf-8 -*- # Form implementation generated from reading ui file 'py.ui' # # Created by: PyQt5 UI code generator 5.15.6 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. from PyQt5 import QtCore, QtGui, QtWidgets class Ui_Form(object): def setupUi(self, Form): Form.setObjectName("Form") Form.resize(1029, 818) self.gridLayout_2 = QtWidgets.QGridLayout(Form) self.gridLayout_2.setObjectName("gridLayout_2") self.graphicsView = QtWidgets.QGraphicsView(Form) self.graphicsView.setMinimumSize(QtCore.QSize(800, 800)) self.graphicsView.setMouseTracking(True) self.graphicsView.setTabletTracking(True) self.graphicsView.setObjectName("graphicsView") self.gridLayout_2.addWidget(self.graphicsView, 0, 0, 1, 1) self.gridLayout = QtWidgets.QGridLayout() self.gridLayout.setObjectName("gridLayout") self.tableWidget = QtWidgets.QTableWidget(Form) self.tableWidget.setMinimumSize(QtCore.QSize(120, 0)) self.tableWidget.setMaximumSize(QtCore.QSize(200, 16777215)) self.tableWidget.setObjectName("tableWidget") self.tableWidget.setColumnCount(2) self.tableWidget.setRowCount(0) item = QtWidgets.QTableWidgetItem() self.tableWidget.setHorizontalHeaderItem(0, item) item = QtWidgets.QTableWidgetItem() self.tableWidget.setHorizontalHeaderItem(1, item) self.gridLayout.addWidget(self.tableWidget, 3, 0, 1, 1) self.CircleROI = QtWidgets.QRadioButton(Form) self.CircleROI.setObjectName("CircleROI") self.gridLayout.addWidget(self.CircleROI, 0, 0, 1, 1) self.MultiRectROI = QtWidgets.QRadioButton(Form) self.MultiRectROI.setObjectName("MultiRectROI") self.gridLayout.addWidget(self.MultiRectROI, 2, 0, 1, 1) self.ClearROIButton = QtWidgets.QPushButton(Form) self.ClearROIButton.setObjectName("ClearROIButton") self.gridLayout.addWidget(self.ClearROIButton, 4, 0, 1, 1) self.RectROI = QtWidgets.QRadioButton(Form) self.RectROI.setObjectName("RectROI") self.gridLayout.addWidget(self.RectROI, 1, 0, 1, 1) self.loadButton = QtWidgets.QPushButton(Form) self.loadButton.setObjectName("loadButton") self.gridLayout.addWidget(self.loadButton, 5, 0, 1, 1) self.gridLayout_2.addLayout(self.gridLayout, 0, 1, 1, 1) self.retranslateUi(Form) QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate Form.setWindowTitle(_translate("Form", "Form")) item = self.tableWidget.horizontalHeaderItem(0) item.setText(_translate("Form", "ROI")) item = self.tableWidget.horizontalHeaderItem(1) item.setText(_translate("Form", "Average")) self.CircleROI.setText(_translate("Form", "CircleROI")) self.MultiRectROI.setText(_translate("Form", "MultiRectROI")) self.ClearROIButton.setText(_translate("Form", "ClearROI")) self.RectROI.setText(_translate("Form", "RectROI")) self.loadButton.setText(_translate("Form", "PushButton"))
ROI库文件仅记录修改的部分
def getArrayRegion(self, arr, img=None, axes=(0, 1), **kwds): """ Return the result of :meth:`~pyqtgraph.ROI.getArrayRegion` masked by the elliptical shape of the ROI. Regions outside the ellipse are set to 0. See :meth:`~pyqtgraph.ROI.getArrayRegion` for a description of the arguments. Note: ``returnMappedCoords`` is not yet supported for this ROI type. """ # Note: we could use the same method as used by PolyLineROI, but this # implementation produces a nicer mask. arr = ROI.getArrayRegion(self, arr, img, axes, **kwds) if arr is None or arr.shape[axes[0]] == 0 or arr.shape[axes[1]] == 0: return arr w = arr.shape[axes[0]] h = arr.shape[axes[1]] ## generate an ellipsoidal mask mask = np.fromfunction(lambda x,y: (((x+0.5)/(w/2.)-1)**2+ ((y+0.5)/(h/2.)-1)**2)**0.5 < 1, (w, h)) mask_buf = mask # reshape to match array axes if axes[0] > axes[1]: mask = mask.T shape = [(n if i in axes else 1) for i,n in enumerate(arr.shape)] mask = mask.reshape(shape) return (arr * mask,mask_buf)
做为一个初创公司口头上的研发经理(光杆司令),在没有(招不到)软件工程师的情况下,只能硬着头皮硬上了。
有一些局部的小瑕疵,看我时间和心情再更新。----2022年5月1日
代码更新见Tomowave: Tomowave代码仓库 (gitee.com)----实现多边形ROI显示优化及右键删除单个ROI,2022年5月5日
已实现实时绘制矩形和圆形ROI,并实时更新计算值,代码见gitee仓库,2022年5月9日
浙公网安备 33010602011771号