最近在使用我们的新型防堵型释放器做气浮试验,以探究释放器的一些物理特性。其中,气泡沿软管长度的形态变化(气泡量、气泡粒径)需要重点关注。故试验过程中使用单反拍摄了大量(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()被调