1. 程式人生 > >基於Python+Pyqt+Opencv的氣浮圖片氣泡特徵識別

基於Python+Pyqt+Opencv的氣浮圖片氣泡特徵識別

       最近在使用我們的新型防堵型釋放器做氣浮試驗,以探究釋放器的一些物理特性。其中,氣泡沿軟管長度的形態變化(氣泡量、氣泡粒徑)需要重點關注。故試驗過程中使用單反拍攝了大量(1000張左右)照片,照片如圖1所示。軟管中有少量粒徑較大氣泡,大部分粒徑較小,較難直接觀測。

                                                                                        圖1 軟管中的氣泡分佈

        為了驗證觀測的可行性,使用Photoshop調圖片的亮度、對比度後進行銳化,銳化圖片如圖2所示。從銳化圖片可以明顯看到微小氣泡的存在。為了驗證統計的可行性,特地讓師弟(在這裡特別感謝李林原同學)從圖片中截取了幾個斷面做氣泡直徑分析。氣泡直徑分析?說起來好像挺高階,其實就是用Photoshop的標尺來量畫素。軟管的外徑是已知的,畫素也是可測量的,一個簡單的比例的關係就可以算出來氣泡大小。

                                                                                         

                                                                                         圖2 銳化後圖片

        但進度不是很理想,僅一張圖片我師弟就忙活了整整一天,想想夾子裡總共1000張左右的圖片,就跟他開玩笑說:慢慢數吧,數完了你碩士就畢業了:)

         所以痛點就在於方案是可行的,但效率是不足的。正巧手上有一本毛星雲著的《OpenCV3程式設計入門》,OpenCV3在影象處理方面功能還是蠻強大的,Photoshop可以實現的功能,理論上都可以使用OpenCV編碼來實現。但OpenCV的短板就在於控制元件只有一個Slider,功能太過於單一,Label、Button、textbox啥啥的啥都沒有,這就需要做一個Client程式來整合OpenCV。讀碩士之前及當中我是斷斷續續做過一年左右的碼農的(這裡要感謝碩導的開明以及高勝經理的培養),但基本上都是做B/S結構的專案,C/S結構的基本上不怎麼涉及,所以選擇一個功能強大的,輕量級的,短時間可以上手的桌面環境就極為重要。上述書中使用的環境為VC++,這個環境下貌似只有Windows API和MFC可選擇,使用Windows API做桌面程式簡直就是噩夢,一條條API慢慢查,慢慢堆,果斷放棄。MFC封裝好一些,但是學習週期較長,且是上世紀末的產物,現在基本淘汰(當然,市面上還能看到不少MFC的程式,可能也算與老程式設計師相容吧:))。在Visual Studio中,還有C sharp可以選擇,但C sharp我也不會,且C sharp下如何呼叫OpenCV我也完全沒有概念。最終,我還是把注意力集中在了Pyqt上,我其實從博一就開始關注Pyqt了,只是一直也沒有地方用,所以也就只是看看了文件和例程,僅僅有些感性認識。Pyqt的優點在於:1、可移植性好;2、與Python的相容性高;3、Python牛逼性強;4、門檻低;關於可移植性,Pyqt脫胎於qt,Windows、Linux(包括類Linux,如MAC)、Android通吃,完全是一處編碼,處處執行。關於Python的牛逼之處,我就不多說了,搞科研的都懂。關於門檻低,Pyqt本身還帶輔助介面設計程式Qt designer,使用pyuic5可以直接將生成的UI檔案轉換成py檔案,直接在py檔案裡面寫邏輯就行。做出來的第一個版本如圖3所示。

                                          

                                                                                         圖3 第一版程式

        本來想將圖片載入後在圖中的QLabel(也就是灰框框)裡面顯示,但我顯然還是太高估了自己的水平(也是因為對Pyqt、Python、numpy根本不熟),所以最終還是選擇了妥協,使用OpenCV的imshow()來顯示圖片。如何顯示不重要,重要的是功能的實現。

                                                                                     圖4 第二版程式

        當功能基本都實現後,還是對UI進行了調整,調整後的程式如圖5所示。

                                             

                                             

                                                                                     圖5 第三版程式

到這裡,就基本上搞定了,識別出來的氣泡邊緣如圖6所示(這是原照片中的一個小的區域性,本來識別出來的氣泡應該是圓形的,但由於拍攝裝置能力有限,以及光影、折射、散射、演算法等因素的原因,導致識別出來的氣泡並不圓,不過由於氣泡本身直徑非常小,這些誤差暫時忽略不計)。

                                                      

                                                                                        圖6 氣泡邊緣識別圖

        邊緣特徵引數記錄如圖7所示。

                              

                                                                                          圖7 邊緣特徵引數

      通過輪廓的面積和周長即可計算出氣泡直徑(這部分就不寫邏輯了,獲得資料後,拖拖Excel就搞定了)

      原始碼如下(按邏輯能夠跑下來,但bug還沒有掃,bug交給師弟慢慢掃:>)

import sys
import cv2
import numpy as np
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QApplication, QWidget, QFileDialog,QMainWindow,QMessageBox 
from PyQt5.QtGui import QImage ,QPixmap, QPainter
from math import *

winName="My Picture"
zoom=0
fname=None #儲存圖片資訊
pic=None #儲存無損圖片
rotatePic=None #儲存無損旋轉圖片
picPath=None #儲存無損圖片路徑
newPic=None #最終生成的無損圖片
handlePic=None #處理中的圖片
modiflyPic=[] #識別處理圖片
thresholdPic=None#閾值處理圖片
zoomWidth=0
zoomHeight=0
picRead=0
leftPoint=None
rightPoint=None
rotateZoompic=None#旋轉處理的圖片
rotate=0

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(500, 159)
        self.tabWidget = QtWidgets.QTabWidget(Form)
        self.tabWidget.setGeometry(QtCore.QRect(10, 10, 480, 131))
        self.tabWidget.setObjectName("tabWidget")
        self.tab = QtWidgets.QWidget()
        self.tab.setObjectName("tab")
        self.openPic = QtWidgets.QPushButton(self.tab)
        self.openPic.setGeometry(QtCore.QRect(5, 10, 90, 30))
        self.openPic.setObjectName("openPic")
        self.zoomUp = QtWidgets.QPushButton(self.tab)
        self.zoomUp.setGeometry(QtCore.QRect(120, 10, 90, 30))
        self.zoomUp.setObjectName("zoomUp")
        self.zoomDown = QtWidgets.QPushButton(self.tab)
        self.zoomDown.setGeometry(QtCore.QRect(240, 10, 90, 30))
        self.zoomDown.setObjectName("zoomDown")
        self.savePic = QtWidgets.QPushButton(self.tab)
        self.savePic.setGeometry(QtCore.QRect(360, 10, 90, 30))
        self.savePic.setObjectName("savePic")
        self.rotate = QtWidgets.QSlider(self.tab)
        self.rotate.setGeometry(QtCore.QRect(10, 50, 321, 31))
        self.rotate.setProperty("value", 0)
        self.rotate.setProperty("maximum",180)
        self.rotate.setProperty("minimum",-180)
        self.rotate.setOrientation(QtCore.Qt.Horizontal)
        self.rotate.setObjectName("rotate")
        self.label = QtWidgets.QLabel(self.tab)
        self.label.setGeometry(QtCore.QRect(168, 85, 20, 20))
        self.label.setObjectName("label")
        self.label_2 = QtWidgets.QLabel(self.tab)
        self.label_2.setGeometry(QtCore.QRect(0, 85, 51, 20))
        self.label_2.setObjectName("label_2")
        self.label_3 = QtWidgets.QLabel(self.tab)
        self.label_3.setGeometry(QtCore.QRect(310, 85, 41, 20))
        self.label_3.setObjectName("label_3")
        self.label_7 = QtWidgets.QLabel(self.tab)
        self.label_7.setGeometry(QtCore.QRect(380, 60, 90, 12))
        self.label_7.setObjectName("label_7")
        self.tabWidget.addTab(self.tab, "")
        self.tab_2 = QtWidgets.QWidget()
        self.tab_2.setObjectName("tab_2")
        self.openBubpic = QtWidgets.QPushButton(self.tab_2)
        self.openBubpic.setGeometry(QtCore.QRect(5, 10, 90, 30))
        self.openBubpic.setObjectName("openBubpic")
        self.lightMod = QtWidgets.QSlider(self.tab_2)
        self.lightMod.setGeometry(QtCore.QRect(105, 15, 300, 20))
        self.lightMod.setProperty("value", 0)
        self.lightMod.setProperty("maximum",200)
        self.lightMod.setProperty("minimum",0)
        self.lightMod.setOrientation(QtCore.Qt.Horizontal)
        self.lightMod.setObjectName("lightMod")
        self.recogPic = QtWidgets.QPushButton(self.tab_2)
        self.recogPic.setGeometry(QtCore.QRect(5, 42, 90, 30))
        self.recogPic.setObjectName("recogPic")
        self.savBubdata = QtWidgets.QPushButton(self.tab_2)
        self.savBubdata.setGeometry(QtCore.QRect(5, 75, 90, 30))
        self.savBubdata.setObjectName("savBubdata")
        self.contrastMod = QtWidgets.QSlider(self.tab_2)
        self.contrastMod.setGeometry(QtCore.QRect(105, 50, 300, 20))
        self.contrastMod.setProperty("value", 100)
        self.contrastMod.setProperty("maximum",300)
        self.contrastMod.setProperty("minimum",100)
        self.contrastMod.setOrientation(QtCore.Qt.Horizontal)
        self.contrastMod.setObjectName("contrastMod")
        self.sharpMod = QtWidgets.QSlider(self.tab_2)
        self.sharpMod.setGeometry(QtCore.QRect(105, 80, 300, 20))
        self.sharpMod.setProperty("value", 120)
        self.sharpMod.setProperty("maximum",255)
        self.sharpMod.setProperty("minimum",0)
        self.sharpMod.setOrientation(QtCore.Qt.Horizontal)
        self.sharpMod.setObjectName("sharpMod")
        self.label_4 = QtWidgets.QLabel(self.tab_2)
        self.label_4.setGeometry(QtCore.QRect(410, 15, 54, 12))
        self.label_4.setObjectName("label_4")
        self.label_5 = QtWidgets.QLabel(self.tab_2)
        self.label_5.setGeometry(QtCore.QRect(410, 50, 60, 12))
        self.label_5.setObjectName("label_5")
        self.label_6 = QtWidgets.QLabel(self.tab_2)
        self.label_6.setGeometry(QtCore.QRect(410, 80, 54, 12))
        self.label_6.setObjectName("label_6")
        self.tabWidget.addTab(self.tab_2, "")
        #訊號繫結
        self.openPic.clicked.connect(self.openfile)
        self.zoomUp.clicked.connect(self.zoomUpfun)
        self.zoomDown.clicked.connect(self.zoomDownfun)
        self.rotate.sliderReleased.connect(self.rotateZoompicfun)
        self.lightMod.sliderReleased.connect(self.lightModify)
        self.contrastMod.sliderReleased.connect(self.contrastModify)
        self.sharpMod.sliderReleased.connect(self.sharpModify)
        self.recogPic.clicked.connect(self.recogPicfun)
        self.savePic.clicked.connect(self.writefile)
        self.retranslateUi(Form)
        self.tabWidget.setCurrentIndex(0)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "Form"))
        self.openPic.setText(_translate("Form", "開啟圖片"))
        self.zoomUp.setText(_translate("Form", "放大"))
        self.zoomDown.setText(_translate("Form", "縮小"))
        self.savePic.setText(_translate("Form", "儲存圖片"))
        self.label.setText(_translate("Form", "0°"))
        self.label_2.setText(_translate("Form", "-180°"))
        self.label_3.setText(_translate("Form", "180°"))
        self.label_7.setText(_translate("Form", "旋轉圖片"))
        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), _translate("Form", "圖片修改"))
        self.openBubpic.setText(_translate("Form", "開啟圖片"))
        self.recogPic.setText(_translate("Form", "識別邊緣"))
        self.savBubdata.setText(_translate("Form", "儲存資料"))
        self.label_4.setText(_translate("Form", "亮度調節"))
        self.label_5.setText(_translate("Form", "對比度調節"))
        self.label_6.setText(_translate("Form", "二值化閾值"))
        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), _translate("Form", "氣泡識別"))
    #訊號處理槽
    #開啟檔案
    def openfile(self):
        fname=QFileDialog.getOpenFileName(self.openPic,'請選擇圖片','.',"Image Files(*.jpg *.png)")
        if fname[0]:
            global pic
            global picRead
            global zoom
            global leftPoint
            global rightPoint
            global handlePic
            global zoomWidth
            global zoomHeight
            pic=self.cv_imread(fname[0])
            #轉為灰度圖
            pic=cv2.cvtColor(pic,cv2.COLOR_BGR2GRAY);
            picRead=1
            if pic.data==0:
                print("圖片讀取錯誤\n")
                return
            zoom=0
            handlePic=None
            leftPoint=None
            rightPoint=None
            zoomWidth=0
            zoomHeight=0
            if pic.shape[1]>pic.shape[0]:
                zoomWidth=500
                zoomHeight=pic.shape[0]*500/pic.shape[1]
            else:
                zoomHeight=500
                zoomWidth=pic.shape[1]*500/pic.shape[0]
            handlePic=cv2.resize(pic,(int(zoomWidth),int(zoomHeight)),interpolation=cv2.INTER_AREA)
            self.finalpic(handlePic)
    #OpemCv中文路徑處理
    def cv_imread(self,filePath):
        cv_img=cv2.imdecode(np.fromfile(filePath,dtype=np.uint8),-1)
        return cv_img
    #儲存高清圖片
    def writefile(self):
        if picRead==0:
            print("沒有載入圖片\n")
            return
        fname=QFileDialog.getSaveFileName(self.savePic,"儲存圖片檔案",'.',"Image Files(*.jpg *.png)")
        picWrite=cv2.imwrite(fname[0],newPic)
        if picWrite==0:
            print("儲存圖片失敗!")
        
    #顯示圖片
    def finalpic(self,finalPic):
        cv2.namedWindow(winName,cv2.WINDOW_AUTOSIZE)
        cv2.setMouseCallback(winName,self.on_mouse)
        key=cv2.imshow(winName,finalPic)
    #滑鼠事件響應
    def on_mouse(self, event, x, y, flags, frames):
        global leftPoint
        global rightPoint
        global handlePic
        global rotateZoompic
        global zoomWidth
        global zoomHeight
        global rotatePic
        global pic
        global newPic
        if picRead==0:
            print("沒有載入圖片\n")
            return
        if event==cv2.EVENT_LBUTTONDOWN:
            leftPoint=(x,y)
        elif event==cv2.EVENT_MOUSEMOVE:
            rightPoint=(x,y)
        elif event==cv2.EVENT_LBUTTONUP:
            #zoomPic=zoomPic(cv2.Rect(leftPoint.x,leftPoint.y,(rightPoint.x-leftPoint.x),(rightPoint.y-leftPoint.y)))
            if rotate==1:
                multiple=rotatePic.shape[1]/rotateZoompic.shape[1]
                handlePic=rotateZoompic[leftPoint[1]:rightPoint[1],leftPoint[0]:rightPoint[0]]
                newPic=rotatePic[int(leftPoint[1]*multiple):int(rightPoint[1]*multiple),int(leftPoint[0]*multiple):int(rightPoint[0]*multiple)]
            else:
                multiple=pic.shape[1]/handlePic.shape[1]
                handlePic=handlePic[leftPoint[1]:rightPoint[1],leftPoint[0]:rightPoint[0]]
                newPic=pic[int(leftPoint[1]*multiple):int(rightPoint[1]*multiple),int(leftPoint[0]*multiple):int(rightPoint[0]*multiple)]
            zoomWidth=handlePic.shape[1]
            zoomHeight=handlePic.shape[0]
            self.finalpic(handlePic)       
    #圖片放大
    def zoomUpfun(self):
        global zoom
        global handlePic
        global zoomWidth
        global zoomHeight
        if picRead==0:
            print("沒有載入圖片\n")
            return
        zoom+=1
        if rotate==1:
            handlePic=cv2.resize(rotateZoompic,(int(rotateZoompic.shape[1]*(1+0.1*zoom)),int(rotateZoompic.shape[0]*(1+0.1*zoom))),interpolation=cv2.INTER_AREA)
        else:
            handlePic=cv2.resize(handlePic,(int(handlePic.shape[1]*(1+0.1*zoom)),int(handlePic.shape[0]*(1+0.1*zoom))),interpolation=cv2.INTER_AREA)
        zoomWidth=handlePic.shape[1]
        zoomHeight=handlePic.shape[0]
        cv2.destroyWindow(winName)
        self.finalpic(handlePic)
    #圖片縮小
    def zoomDownfun(self):
        global zoom
        global handlePic
        global zoomWidth
        global zoomHeight
        if picRead==0:
            print("沒有載入圖片\n")
            return
        zoom-=1
        if rotate==1:
            handlePic=cv2.resize(rotateZoompic,(int(rotateZoompic.shape[1]*(1+0.1*zoom)),int(rotateZoompic.shape[0]*(1+0.1*zoom))),interpolation=cv2.INTER_AREA)
        else:
            handlePic=cv2.resize(handlePic,(int(handlePic.shape[1]*(1+0.1*zoom)),int(handlePic.shape[0]*(1+0.1*zoom))),interpolation=cv2.INTER_AREA)
        handlePic=cv2.resize(handlePic,(int(handlePic.shape[1]*(1+0.1*zoom)),int(handlePic.shape[0]*(1+0.1*zoom))),interpolation=cv2.INTER_AREA)
        zoomWidth=handlePic.shape[1]
        zoomHeight=handlePic.shape[0]
        cv2.destroyWindow(winName)
        self.finalpic(handlePic)
    #旋轉圖片
    def rotateZoompicfun(self):
        global handlePic
        global zoomWidth
        global zoomHeight
        global rotateZoompic
        global rotate
        global rotatePic
        global pic
        if picRead==0:
            print("沒有載入圖片\n")
            return
        else:
            angle=self.rotate.value()
            #旋轉縮放後圖片——仿射變換
            heightZoomnew=int(zoomWidth*fabs(sin(radians(angle)))+zoomHeight*fabs(cos(radians(angle))))
            widthZoomnew=int(zoomHeight*fabs(sin(radians(angle)))+zoomWidth*fabs(cos(radians(angle))))
            rot=cv2.getRotationMatrix2D((zoomWidth/2,zoomHeight/2),angle,1)
            rot[0,2]+=(widthZoomnew-zoomWidth)/2
            rot[1,2]+=(heightZoomnew-zoomHeight)/2
            rotateZoompic=cv2.warpAffine(handlePic,rot,(widthZoomnew,heightZoomnew))
            #旋轉高清圖片——仿射變換
            heightnew=int(pic.shape[1]*fabs(sin(radians(angle)))+pic.shape[0]*fabs(cos(radians(angle))))
            widthnew=int(pic.shape[0]*fabs(sin(radians(angle)))+pic.shape[1]*fabs(cos(radians(angle))))
            rot=cv2.getRotationMatrix2D((pic.shape[1]/2,pic.shape[0]/2),angle,1)
            rot[0,2]+=(widthnew-pic.shape[1])/2
            rot[1,2]+=(heightnew-pic.shape[0])/2
            rotatePic=cv2.warpAffine(pic,rot,(widthnew,heightnew))
            rotate=1;
            self.finalpic(rotateZoompic)
    #亮度調節
    def lightModify(self):
        global modiflyPic
        global newPic
        if picRead==0:
            print("沒有載入圖片\n")
            return
        modiflyPic=newPic
        width=newPic.shape[1]
        height=newPic.shape[0]
        for row in range(0,height):
            for col in range(0,width):
                modiflyPic[row,col]=self.checkPixels((self.contrastMod.value()*0.01)*newPic[row,col]+self.lightMod.value())
        self.finalpic(modiflyPic)
    #對比度調節
    def contrastModify(self):
        global modiflyPic
        if picRead==0:
            print("沒有載入圖片\n")
            return
        modiflyPic=newPic
        width=newPic.shape[1]
        height=newPic.shape[0]
        for row in range(0,height):
            for col in range(0,width):
                modiflyPic[row,col]=self.checkPixels((self.contrastMod.value()*0.01)*newPic[row,col]+self.lightMod.value())
        self.finalpic(modiflyPic)     
    #閾值檢查
    def checkPixels(self,value):
        if value<0:
            value=0
        elif value>255:
            value=255
        return int(value)
    #二值化閾值調節
    def sharpModify(self):
        global modiflyPic
        global thresholdPic
        ret,thresholdPic=cv2.threshold(modiflyPic,self.sharpMod.value(),255,cv2.THRESH_BINARY)#與opencv不同,返回兩個引數
        self.finalpic(thresholdPic)
    #識別邊緣
    def recogPicfun(self):
        global thresholdPic
        global modiflyPic
        image,contours,hierarchy=cv2.findContours(thresholdPic, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE);
        img=cv2.drawContours(modiflyPic,contours,-1,(0,0,255),3)
        cv2.imshow('ttt',img)
if __name__ == '__main__':
   """
   主函式
   """
   app = QApplication(sys.argv)
   #app = QApplication(sys.argv),每一個pyqt程式必須建立一個application物件,
   #sys.argv是命令列引數,可以通過命令啟動的時候傳遞引數。
   mainWindow = QWidget()
   #生成過一個例項(物件), mainWindow是例項(物件)的名字,可以隨便起。
   ui = Ui_Form()
   ui.setupUi(mainWindow)
   mainWindow.show()  #用來顯示視窗
   sys.exit(app.exec_())#exec_()方法的作用是“進入程式的主迴圈直到exit()被調