基於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()被調