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日

posted @ 2022-05-01 17:59  昊天一怪  阅读(1825)  评论(0)    收藏  举报