From ca10e46aa63d2476039cb400bbbcb4503509a7a7 Mon Sep 17 00:00:00 2001 From: mvgorcum Date: Sat, 26 Sep 2020 01:40:54 +0200 Subject: [PATCH] added contact angle analysis --- QT_sessile_drop_analysis.py | 53 +++++++++++++++++++++++++------------ edge_analysis.py | 43 +++++++++++++++++++++++++++--- 2 files changed, 75 insertions(+), 21 deletions(-) diff --git a/QT_sessile_drop_analysis.py b/QT_sessile_drop_analysis.py index 160109b..37de70a 100644 --- a/QT_sessile_drop_analysis.py +++ b/QT_sessile_drop_analysis.py @@ -6,7 +6,9 @@ import sys from pathlib import Path from edge_detection import linear_subpixel_detection as linedge +from edge_analysis import analysis import numpy as np +import pandas as pd import threading from time import sleep import datetime @@ -63,8 +65,9 @@ def stop(self): def getnextframe(self): ret, org_frame = self.cap.read() + framenumber = self.cap.get(cv2.CAP_PROP_POS_FRAMES) if ret: - return org_frame + return org_frame,framenumber else: self.is_running=False self.stop() @@ -109,7 +112,7 @@ def getnextframe(self): """ if len(self.framebuffer)>=1: - return self.framebuffer.pop(0) + return self.framebuffer.pop(0),self.framecaptime.pop(0) else: return -1 @@ -147,7 +150,8 @@ class MainWindow(QtWidgets.QMainWindow): updateVideo = pyqtSignal(np.ndarray) updateLeftEdge = pyqtSignal(np.ndarray,np.ndarray) updateRightEdge = pyqtSignal(np.ndarray,np.ndarray) - + updatePlotLeft = pyqtSignal(np.ndarray,np.ndarray) + updatePlotRight = pyqtSignal(np.ndarray,np.ndarray) def __init__(self, *args, **kwargs): super(MainWindow, self).__init__(*args, **kwargs) uic.loadUi('Mainwindow.ui', self) @@ -161,6 +165,14 @@ def __init__(self, *args, **kwargs): self.updateLeftEdge.connect(self.LeftEdgeItem.setData) self.updateRightEdge.connect(self.RightEdgeItem.setData) + self.ThetaLeftPlot=pg.PlotCurveItem() + self.ThetaRightPlot=pg.PlotCurveItem() + self.PlotItem=self.PlotWidget.getPlotItem() + self.PlotItem.addItem(self.ThetaLeftPlot) + self.PlotItem.addItem(self.ThetaRightPlot) + self.updatePlotLeft.connect(self.ThetaLeftPlot.setData) + self.updatePlotRight.connect(self.ThetaRightPlot.setData) + self.BaseLine=pg.LineSegmentROI([(15,90),(100,90)],pen='r') self.CropRoi=pg.RectROI([10,10],[110,110]) self.CropRoi.addScaleHandle([0,0],[1,1]) @@ -172,6 +184,7 @@ def __init__(self, *args, **kwargs): self.CameraToggleButton.clicked.connect(self.CameraToggle) self.FrameSource=FrameSupply() + self.MeasurementResult=pd.DataFrame(columns=['thetaleft', 'thetaright', 'contactpointleft','contactpointright','volume','time']) def openCall(self): if self.FrameSource.is_running: @@ -183,8 +196,9 @@ def openCall(self): FrameWidth,FrameHeight=self.FrameSource.getframesize() self.CropRoi.setPos([FrameWidth*.1,FrameHeight*.1]) self.CropRoi.setSize([FrameWidth*.8,FrameHeight*.8]) - self.BaseLine.setPos([FrameWidth*.2,FrameHeight*.7]) - self.VideoItem.setImage(cv2.cvtColor(self.FrameSource.getnextframe(), cv2.COLOR_BGR2RGB),autoRange=True) + #self.BaseLine.setPos([FrameWidth*.2,FrameHeight*.7]) + firstframe,_=self.FrameSource.getnextframe() + self.VideoItem.setImage(cv2.cvtColor(firstframe, cv2.COLOR_BGR2RGB),autoRange=True) @@ -221,29 +235,34 @@ def CameraCapture(self): def RunAnalysis(self): while self.StartStopButton.isChecked(): - org_frame = self.FrameSource.getnextframe() + org_frame,framecaptime = self.FrameSource.getnextframe() if not np.all(org_frame==-1): + #get crop and save coordinate transformation cropcoords=self.CropRoi.getArraySlice(org_frame, self.VideoItem.getImageItem(), returnSlice=False) + verticalCropOffset=0.5+cropcoords[0][0][0] + horizontalCropOffset=0.5+cropcoords[0][1][0] cropped=org_frame[cropcoords[0][0][0]:cropcoords[0][0][1],cropcoords[0][1][0]:cropcoords[0][1][1],:] - baselineobj=self.BaseLine.getArraySlice(org_frame, self.VideoItem.getImageItem(), returnSlice=False) - print(baselineobj) - baseinput=baselineobj[0] - print(baseinput) + + #get baseline positions and extrapolate to the edge of the crop + baselineobj=self.BaseLine.getSceneHandlePositions() + baseinput=[[baselineobj[0][1].x(),baselineobj[0][1].y()],[baselineobj[1][1].x(),baselineobj[1][1].y()]] del baselineobj - #TODO fix baseline here, coordinates don't seem to correspond rightbasepoint=np.argmax([baseinput[0][0],baseinput[1][0]]) baseslope=np.float(baseinput[rightbasepoint][1]-baseinput[1-rightbasepoint][1])/(baseinput[rightbasepoint][0]-baseinput[1-rightbasepoint][0]) base=np.array([[0,baseinput[0][1]-baseslope*baseinput[0][0]],[cropped.shape[1],baseslope*cropped.shape[1]+baseinput[0][1]-baseslope*baseinput[0][0]]]) - print(base) gray = cv2.cvtColor(cropped.astype('uint8'), cv2.COLOR_BGR2GRAY) thresh, _ =cv2.threshold(gray, 0, 255, cv2.THRESH_OTSU) CroppedEdgeLeft,CroppedEdgeRight=linedge(gray,thresh) - EdgeLeft=CroppedEdgeLeft+0.5+cropcoords[0][1][0] - EdgeRight=CroppedEdgeRight+0.5+cropcoords[0][1][0] - + EdgeLeft=CroppedEdgeLeft+horizontalCropOffset + EdgeRight=CroppedEdgeRight+horizontalCropOffset + contactpointleft, contactpointright, thetal, thetar, dropvolume = analysis(EdgeLeft,EdgeRight,base,baseslope,cropped.shape,k=100,PO=2) + newrow={'thetaleft':thetal, 'thetaright':thetar, 'contactpointleft':contactpointleft,'contactpointright':contactpointright,'volume':dropvolume,'time':framecaptime} + self.MeasurementResult=self.MeasurementResult.append(newrow,ignore_index=True) self.updateVideo.emit(cv2.cvtColor(org_frame, cv2.COLOR_BGR2RGB)) - self.updateLeftEdge.emit(EdgeLeft,np.arange(0,len(EdgeLeft))+0.5+cropcoords[0][0][0]) - self.updateRightEdge.emit(EdgeRight,np.arange(0,len(EdgeRight))+0.5+cropcoords[0][0][0]) + self.updateLeftEdge.emit(EdgeLeft,np.arange(0,len(EdgeLeft))+verticalCropOffset) + self.updateRightEdge.emit(EdgeRight,np.arange(0,len(EdgeRight))+verticalCropOffset) + self.updatePlotLeft.emit(self.MeasurementResult['time'].to_numpy(),self.MeasurementResult['thetaleft'].to_numpy()) + self.updatePlotRight.emit(self.MeasurementResult['time'].to_numpy(),self.MeasurementResult['thetaright'].to_numpy()) else: sleep(0.0001) if (not self.FrameSource.is_running and len(self.FrameSource.framebuffer)<1): diff --git a/edge_analysis.py b/edge_analysis.py index 4459d7e..7e7d438 100644 --- a/edge_analysis.py +++ b/edge_analysis.py @@ -1,10 +1,45 @@ -def analysis(edgeleft,edgeright,baseinput,framesize): +def analysis(edgeleft,edgeright,baseinput,baseslope,framesize,k=100,PO=2): """ - Analizes the detected edge of the drop with the set baseline to + Analyzes the detected edge of the drop with the set baseline to give the contact angle, contact line position, and drop volume + k represents the number of pixels up from the baseline to fit, PO is the + order of the polyfit used to fit the edge of the drop + Returns contactpoints left and right, theta left and right, and drop volume """ from shapely.geometry import LineString import numpy as np - #extend the baseline to the edge of the frame (in case the drop grows) - baseline=LineString(base) #using shapely we construct baseline + baseline=LineString(baseinput) + rightline=LineString(np.column_stack((edgeright,(range(0,framesize[0]))))) + leftline=LineString(np.column_stack((edgeleft,(range(0,framesize[0]))))) + leftcontact=baseline.intersection(leftline) + #print(leftcontact) + rightcontact=baseline.intersection(rightline) + fitpointsleft=edgeleft[range(np.int(np.floor(leftcontact.y)),np.int(np.floor(leftcontact.y)-k),-1)] + if any(fitpointsleft==0): + fitpointsleft=np.delete(fitpointsleft,range(np.argmax(fitpointsleft==0),k)) + fitpointsright=edgeright[range(np.int(np.floor(rightcontact.y)),np.int(np.floor(rightcontact.y)-k),-1)] + if any(fitpointsright==0): + fitpointsright=np.delete(fitpointsright,range(np.argmax(fitpointsright==0),k)) + + leftfit=np.polyfit(range(0,fitpointsleft.shape[0]),fitpointsleft,PO) + leftvec=np.array([1,leftfit[PO-1]]) + + rightfit=np.polyfit(range(0,fitpointsright.shape[0]),fitpointsright,PO) + rightvec=np.array([1,rightfit[PO-1]]) + + basevec=np.array([-baseslope,1]) + thetal=np.arccos(np.dot(basevec,leftvec)/(np.sqrt(np.dot(basevec,basevec))*np.sqrt(np.dot(leftvec,leftvec))))*180/np.pi + thetar=180-np.arccos(np.dot(basevec,rightvec)/(np.sqrt(np.dot(basevec,basevec))*np.sqrt(np.dot(rightvec,rightvec))))*180/np.pi + contactpointright=rightfit[PO] + contactpointleft=leftfit[PO] + dropvolume=0 + for height in range (0,min(np.int(np.floor(leftcontact.y)),np.int(np.floor(rightcontact.y)))): + dropvolume=dropvolume+np.pi*np.square((edgeright[height]-edgeleft[height])/2) + #using cylindrical slice we calculate the remaining volume + slantedbasediff=max(np.floor(leftcontact.y),np.floor(rightcontact.y))-min(np.floor(leftcontact.y),np.floor(rightcontact.y)) + #we assume that the radius is constant over the range of the slanted baseline, for small angles this is probably accurate, but for larger angles this can result in a significant error. + baseradius=(edgeright[np.int(min(np.floor(leftcontact.y),np.floor(rightcontact.y)))]-edgeleft[np.int(min(np.floor(leftcontact.y),np.floor(rightcontact.y)))])/2 + dropvolume=dropvolume+.5*np.pi*np.square(baseradius)*slantedbasediff + + return contactpointleft, contactpointright, thetal, thetar, dropvolume