800 lines
34 KiB
Python
800 lines
34 KiB
Python
#!/usr/bin/env python
|
|
|
|
# Surface Tension Calculation via Pendant Drop Method (Bashforth-Adams)
|
|
# Created by : Ren Tristan A. de la Cruz
|
|
# Created on : 2016 July 26
|
|
# Last Modified : 2025 January 27
|
|
# Contact : rentristandelacruz@gmail.com
|
|
# Contact : radelacruz@up.edu.ph
|
|
|
|
# ========== IMPORTS ==========
|
|
|
|
import math
|
|
import copy
|
|
import time
|
|
import sys
|
|
import cv2
|
|
import multiprocessing
|
|
import numpy as np
|
|
import scipy.optimize #as spo
|
|
|
|
# ========== READ THE IMAGE & EXTRACT THE EDGE COORDINATES ==========
|
|
|
|
def ExtractEdgeCoordinate(_ImageFilePath, _OutputFileName, _SyringeWidth):
|
|
|
|
# Detect the edges of the image.
|
|
# All edge pixels will white(255) and rest of the pixels will be black(0).
|
|
Image = cv2.imread(_ImageFilePath, cv2.IMREAD_COLOR)
|
|
EdgeImage = cv2.Canny(Image, 100, 200)
|
|
|
|
# Find the first edge point (white pixel).
|
|
# It is assumed that first edge point part of the syringe.
|
|
NumOfRow, NumOfCol = EdgeImage.shape
|
|
EdgePoint = (0, 0)
|
|
EdgePointFlag = 0
|
|
for RowIndex in range(NumOfRow):
|
|
for ColIndex in range(NumOfCol):
|
|
if EdgeImage[RowIndex, ColIndex] == 255:
|
|
EdgePoint = (RowIndex, ColIndex)
|
|
EdgePointFlag = 1
|
|
break
|
|
if EdgePointFlag == 1:
|
|
break
|
|
|
|
|
|
# Color the all the syringe and droplet pixels differently (70).
|
|
# Determine the left-most, right-most, and bottom droplet pixels.
|
|
EdgeImage[EdgePoint] = 70
|
|
DropletEdgePoints = []
|
|
DropletEdgePoints.append(EdgePoint)
|
|
LeftMostCol = 9999999999
|
|
RightMostCol = 0
|
|
BottomMostRow = 0
|
|
BottomMostRow2 = 0
|
|
CenterCol = 0
|
|
|
|
while (len(DropletEdgePoints) != 0):
|
|
CurrentPoint = DropletEdgePoints[0]
|
|
DropletEdgePoints.remove(CurrentPoint)
|
|
NeighborPoints = []
|
|
for RowOffset in range(-3,4, 1):
|
|
for ColOffset in range(-3,4,1):
|
|
if(RowOffset != 0 or ColOffset != 0):
|
|
NeighborPoints.append((CurrentPoint[0]+RowOffset, CurrentPoint[1]+ColOffset))
|
|
for PointRow, PointCol in NeighborPoints:
|
|
if (0 <= PointRow and PointRow < NumOfRow) and (0 <= PointCol and PointCol < NumOfCol):
|
|
if EdgeImage[PointRow][PointCol] == 255:
|
|
DropletEdgePoints.append((PointRow, PointCol))
|
|
EdgeImage[PointRow, PointCol] = 70
|
|
if PointRow > BottomMostRow:
|
|
BottomMostRow = PointRow
|
|
if PointCol > RightMostCol:
|
|
RightMostCol = PointCol
|
|
if PointCol < LeftMostCol:
|
|
LeftMostCol = PointCol
|
|
|
|
CenterCol = (LeftMostCol + RightMostCol)//2
|
|
for Row in range(BottomMostRow + 1):
|
|
if EdgeImage[Row][CenterCol] == 70:
|
|
BottomMostRow2 = Row
|
|
BottomMostRow = BottomMostRow2
|
|
|
|
|
|
# Determine the transition row between the syringe pixels and the droplet pixels.
|
|
PreviousWidth = 0
|
|
WidthIncCount = 5
|
|
WidthHistory = [0]*(WidthIncCount+1)
|
|
TransitionRow = 0
|
|
|
|
for Row in range(BottomMostRow):
|
|
LeftEdgeCol = 0
|
|
RightEdgeCol = 9999999
|
|
for Col in range(LeftMostCol-1, RightMostCol+1, 1):
|
|
if EdgeImage[Row,Col] == 70:
|
|
LeftEdgeCol = Col
|
|
break
|
|
for Col in range(RightMostCol+1, LeftMostCol-1, -1):
|
|
if EdgeImage[Row,Col] == 70:
|
|
RightEdgeCol = Col
|
|
break
|
|
CurrentWidth = RightEdgeCol - LeftEdgeCol
|
|
if (CurrentWidth != PreviousWidth) and (0 < CurrentWidth) and (CurrentWidth < (9999999-NumOfCol)):
|
|
for i in range(WidthIncCount):
|
|
WidthHistory[i] = WidthHistory[i+1]
|
|
WidthHistory[WidthIncCount] = CurrentWidth
|
|
WidthIncreaseCount = 0
|
|
for i in range(WidthIncCount):
|
|
if WidthHistory[i] < WidthHistory[i+1]:
|
|
WidthIncreaseCount += 1
|
|
if WidthIncreaseCount == WidthIncCount:
|
|
TransitionRow = Row
|
|
break
|
|
if (0 < CurrentWidth) and (CurrentWidth < (9999999-NumOfCol)):
|
|
PreviousWidth = CurrentWidth
|
|
|
|
|
|
# Determine the width of the syringe in terms of pixel count
|
|
WidthSum = 0
|
|
WidthCount = 0
|
|
SyringeLevel = int(0.75 * TransitionRow)
|
|
for Row in range(TransitionRow):
|
|
RightEdgeCol = 9999999
|
|
LeftEdgeCol = 0
|
|
for Col in range(LeftMostCol, CenterCol + 1, 1):
|
|
if EdgeImage[Row, Col] == 70:
|
|
EdgeImage[Row, Col] = 255
|
|
LeftEdgeCol = Col
|
|
break
|
|
for Col in range(RightMostCol, CenterCol - 1, -1):
|
|
if EdgeImage[Row, Col] == 70:
|
|
EdgeImage[Row, Col] = 255
|
|
RightEdgeCol = Col
|
|
break
|
|
CurrentWidth = RightEdgeCol - LeftEdgeCol
|
|
if WidthCount <= SyringeLevel:
|
|
if (0 < CurrentWidth) and (CurrentWidth < (9999999-NumOfCol)):
|
|
WidthSum += CurrentWidth
|
|
WidthCount += 1
|
|
AverageWidth = float(WidthSum)/float(WidthCount)
|
|
|
|
|
|
# Extract the pixel coordinates of all droplet pixels (color: 70)
|
|
PositiveEdgePoints = []
|
|
NegativeEdgePoints = []
|
|
for Row in range(BottomMostRow, TransitionRow-1 ,-1):
|
|
for Col in range(LeftMostCol, CenterCol + 1, 1):
|
|
if EdgeImage[Row, Col] == 70:
|
|
NegativeEdgePoints.append((Row, Col))
|
|
break
|
|
for Col in range(RightMostCol, CenterCol, -1):
|
|
if EdgeImage[Row, Col] == 70:
|
|
PositiveEdgePoints.append((Row, Col))
|
|
break
|
|
|
|
|
|
# Scale the point coordinates by using the syringe length
|
|
SyringeWidth = _SyringeWidth
|
|
ScalingFactor = SyringeWidth/AverageWidth
|
|
ScaledNegativeEdgePoints = []
|
|
NegativeCoordinatesFile = open(_OutputFileName+"_Negative.dat", 'w')
|
|
for Row, Col in NegativeEdgePoints:
|
|
Row2 = (ScalingFactor*float(BottomMostRow-Row)) + 0.0001
|
|
Col2 = ScalingFactor*float(Col - CenterCol)
|
|
NegativeCoordinatesFile.write(str(Col2) + " " + str(Row2) + "\n")
|
|
ScaledNegativeEdgePoints.append((Col2,Row2))
|
|
NegativeCoordinatesFile.close()
|
|
|
|
ScaledPositiveEdgePoints = []
|
|
PositiveCoordinatesFile = open(_OutputFileName+"_Positive.dat", 'w')
|
|
for Row, Col in PositiveEdgePoints:
|
|
Row2 = (ScalingFactor*float(BottomMostRow-Row)) + 0.0001
|
|
Col2 = ScalingFactor*float(Col - CenterCol)
|
|
PositiveCoordinatesFile.write(str(Col2) + " " + str(Row2) + "\n")
|
|
ScaledPositiveEdgePoints.append((Col2, Row2))
|
|
PositiveCoordinatesFile.close()
|
|
|
|
|
|
# Draw the detected edge points of the droplet.
|
|
Image2 = copy.deepcopy(Image)
|
|
for Row in range(0, NumOfRow):
|
|
for Col in range(0, NumOfCol):
|
|
Image[(Row,Col)] = 0
|
|
EdgeImage[(Row,Col)] = 0
|
|
for Point in PositiveEdgePoints:
|
|
Image[Point] = (0, 0, 255)
|
|
EdgeImage[Point] = 255
|
|
for Point in NegativeEdgePoints:
|
|
Image[Point] = (0, 0, 255)
|
|
EdgeImage[Point] = 255
|
|
|
|
# Draw the center column of the drop
|
|
cv2.line(Image, (CenterCol, TransitionRow), (CenterCol, BottomMostRow), (0, 255, 0), 1) # image, start, end, color(BGR), thickness
|
|
|
|
MaxWidthRatio = 0.0
|
|
for Row in range(BottomMostRow, TransitionRow-1 ,-1):
|
|
RightCol = 99999
|
|
LeftCol = -1
|
|
for Col in range(LeftMostCol, CenterCol + 1, 1):
|
|
if EdgeImage[Row, Col] == 255:
|
|
LeftCol = Col
|
|
break
|
|
for Col in range(RightMostCol, CenterCol, -1):
|
|
if EdgeImage[Row, Col] == 255:
|
|
RightCol = Col
|
|
break
|
|
WidthRatio = float(RightCol - LeftCol)/float(RightMostCol-LeftMostCol)
|
|
if WidthRatio < 1.0 and WidthRatio > MaxWidthRatio:
|
|
MaxWidthRatio = WidthRatio
|
|
|
|
RowWithMaxWidth = []
|
|
for Row in range(BottomMostRow, TransitionRow-1 ,-1):
|
|
RightCol = 99999
|
|
LeftCol = -1
|
|
for Col in range(LeftMostCol, CenterCol + 1, 1):
|
|
if EdgeImage[Row, Col] == 255:
|
|
LeftCol = Col
|
|
break
|
|
for Col in range(RightMostCol, CenterCol, -1):
|
|
if EdgeImage[Row, Col] == 255:
|
|
RightCol = Col
|
|
break
|
|
WidthRatio = float(RightCol - LeftCol)/float(RightMostCol-LeftMostCol)
|
|
if WidthRatio == MaxWidthRatio:
|
|
RowWithMaxWidth.append((Row, LeftCol, RightCol))
|
|
|
|
# Draw the main diameter
|
|
n = len(RowWithMaxWidth)
|
|
n = int(n/2)
|
|
Row = RowWithMaxWidth[n][0]
|
|
LeftCol = RowWithMaxWidth[n][1]
|
|
RightCol = RowWithMaxWidth[n][2]
|
|
cv2.line(Image, (LeftCol, Row), (RightCol, Row), (0, 255, 0), 1)
|
|
|
|
# Measure and draw the ten diameters
|
|
Diameters = []
|
|
MaxWidth = RightMostCol - LeftMostCol
|
|
for i in range(1, 13, 1):
|
|
for Col in range(LeftMostCol, CenterCol + 1, 1):
|
|
if EdgeImage[BottomMostRow-int(i*MaxWidth/10.0), Col] == 255:
|
|
LeftCol = Col
|
|
break
|
|
for Col in range(RightMostCol, CenterCol, -1):
|
|
if EdgeImage[BottomMostRow-int(i*MaxWidth/10.0), Col] == 255:
|
|
RightCol = Col
|
|
break
|
|
Diameters.append(RightCol-LeftCol)
|
|
cv2.line(Image, (LeftCol, BottomMostRow-int(i*MaxWidth/10.0)), (RightCol, BottomMostRow-int(i*MaxWidth/10.0)), (255, 0, 0), 1)
|
|
|
|
#DELETE THIS
|
|
#cv2.imwrite("1.jpg", Image)
|
|
#sys.exit()
|
|
|
|
|
|
# Scale the 12 drop diameters
|
|
for i in range(12):
|
|
Diameters[i] *= ScalingFactor
|
|
|
|
ScaledMaxWidth = ScalingFactor*float(MaxWidth)
|
|
ScaledUpWidth = ScalingFactor*float(RightCol - LeftCol)
|
|
return ScaledPositiveEdgePoints, ScaledNegativeEdgePoints, ScaledMaxWidth, Diameters
|
|
|
|
|
|
|
|
# ========== RK4 for BASHFORTH-ADAMS FORMULATION ==========
|
|
|
|
def BashforthAdamsRK4(_afRK4Table, _fApexCurvature, _fShapeFactor, _fSign):
|
|
A = _fApexCurvature
|
|
A2 = 2.0/A
|
|
B = _fShapeFactor
|
|
KX0 = 1.0
|
|
KT0 = 1.0
|
|
Inv6 = 1.0/6.0
|
|
fTotalDiffSquared = 0.0
|
|
n = 1
|
|
while n < len(_afRK4Table):
|
|
x = _afRK4Table[n-1][3] # predicted x
|
|
y = _afRK4Table[n-1][1] # actual y
|
|
t = _afRK4Table[n-1][4] # predicted angle
|
|
dy = _afRK4Table[n][2] # actual y diff
|
|
AG = 1.0 #AG: Added
|
|
ydy = y + 0.5 * dy
|
|
KX1 = dy * (1.0 / math.tan(t))
|
|
KT1 = dy * (1.0 / math.sin(t)) * (A2 - (B * y) - (math.sin(t) / (AG * x))) # A -> AG
|
|
KX2 = dy * (1.0 / math.tan(t + 0.5 * KT1))
|
|
STKT1 = math.sin(t + 0.5 * KT1)
|
|
KT2 = dy * (1.0 / STKT1) * (A2 - (B * ydy) - (STKT1 / (AG * (x + 0.5 * KX1)))) # A -> AG
|
|
KX3 = dy * (1.0 / math.tan(t + 0.5 * KT2))
|
|
STKT2 = math.sin(t + 0.5 * KT2)
|
|
KT3 = dy * (1.0 / STKT2) * (A2 - (B * ydy) - (STKT2 / (AG * (x + 0.5 * KX2)))) # A -> AG
|
|
KX4 = dy * (1.0 / math.tan(t + KT3))
|
|
STKT3 = math.sin(t + KT3)
|
|
KT4 = dy * (1.0 / STKT3) * (A2 - (B * (y + dy)) - (STKT3 / (AG * (x + KX3)))) # A -> AG
|
|
KX = (KX1 + 2.0 * (KX2 + KX3) + KX4) * Inv6
|
|
KT = (KT1 + 2.0 * (KT2 + KT3) + KT4) * Inv6
|
|
_afRK4Table[n][3] = x + KX
|
|
_afRK4Table[n][4] = t + KT
|
|
fTotalDiffSquared += (_afRK4Table[n][0] - (_fSign * _afRK4Table[n][3])) ** 2
|
|
n += 1
|
|
return fTotalDiffSquared
|
|
|
|
def MakeRK4table(_edgePoints):
|
|
InitialValues = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0 , 0, 0, 0]
|
|
|
|
rk4table = []
|
|
rk4table.append(InitialValues)
|
|
yPrev = 0.0
|
|
for x, y in _edgePoints:
|
|
EntryValues = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
|
EntryValues[0] = x
|
|
EntryValues[1] = y
|
|
EntryValues[2] = y - yPrev
|
|
yPrev = y
|
|
rk4table.append(EntryValues)
|
|
|
|
return rk4table
|
|
"""
|
|
RK4NegTable = []
|
|
RK4NegTable.append(InitialValues)
|
|
yPrev = 0.0
|
|
for x, y in _NegEdgePoints:
|
|
EntryValues = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
|
EntryValues[0] = x
|
|
EntryValues[1] = y
|
|
EntryValues[2] = y - yPrev
|
|
yPrev = y
|
|
RK4NegTable.append(EntryValues)
|
|
|
|
return RK4NegTable
|
|
"""
|
|
|
|
# ========== FINDING OPTIMAL PARAMETERS for BASHFORTH-ADAMS SOLUTION ==========
|
|
def FindOptimalParameters(_PosEdgePoints, _NegEdgePoints, _ARange_radCurv, _BRange_shapeFact, _XRange, _TRange_angle, _Params, _Ticks):
|
|
|
|
RK4PosTable = MakeRK4table(_PosEdgePoints)
|
|
RK4NegTable = MakeRK4table(_NegEdgePoints)
|
|
|
|
MinimumRMSD = 999999.0
|
|
BestParameters = [0, 0, 0, 0]
|
|
for A_radCurv in _ARange_radCurv:
|
|
for B_shapeFact in _BRange_shapeFact:
|
|
for X in _XRange:
|
|
for T_angle in _TRange_angle:
|
|
# x, y, dy, x*, t*, KX1, KT1, KX2, KT2, KX3, KT3, KX4, KT4
|
|
fTotalDiffSquared = 0.0
|
|
RK4PosTable[0][3] = X
|
|
RK4PosTable[0][4] = T_angle
|
|
RK4NegTable[0][3] = X
|
|
RK4NegTable[0][4] = T_angle
|
|
fTotalDiffSquared += BashforthAdamsRK4(RK4PosTable, A_radCurv, B_shapeFact, 1.0)
|
|
fTotalDiffSquared += BashforthAdamsRK4(RK4NegTable, A_radCurv, B_shapeFact, -1.0)
|
|
|
|
CurrentRMSD = (fTotalDiffSquared/(len(_PosEdgePoints) + len(_NegEdgePoints)))**0.5
|
|
if CurrentRMSD < MinimumRMSD:
|
|
MinimumRMSD = CurrentRMSD
|
|
BestParameters = [A_radCurv, B_shapeFact, X, T_angle]
|
|
_Ticks.put("#")
|
|
|
|
A_radCurv = BestParameters[0]
|
|
B_shapeFact = BestParameters[1]
|
|
X = BestParameters[2]
|
|
T_angle = BestParameters[3]
|
|
ST = (9.8 * A_radCurv) / B_shapeFact
|
|
_Params.put((MinimumRMSD, A_radCurv, B_shapeFact, X, T_angle))
|
|
|
|
|
|
# ========== FINDING OPTIMAL PARAMETERS USING MINIMZATION ==========
|
|
def OptimizeParameters(_PosEdgePoints, _NegEdgePoints, _AGuess_radCurv, _BGuess_shapeFact, _XGuess, _TGuess_angle):
|
|
|
|
RK4PosTable = MakeRK4table(_PosEdgePoints)
|
|
RK4NegTable = MakeRK4table(_NegEdgePoints)
|
|
"""
|
|
ARG_RAD = 0
|
|
ARG_SHAPE = 1
|
|
ARG_X = 2
|
|
ARG_ANGLE = 3
|
|
"""
|
|
|
|
def msd(args):
|
|
A_radCurv = args[0]
|
|
B_shapeFact = args[1]
|
|
X = args[2]
|
|
T_angle = args[3]
|
|
|
|
RK4PosTable[0][3] = X
|
|
RK4PosTable[0][4] = T_angle
|
|
RK4NegTable[0][3] = X
|
|
RK4NegTable[0][4] = T_angle
|
|
fTotalDiffSquared = BashforthAdamsRK4(RK4PosTable, A_radCurv, B_shapeFact, 1.0) + BashforthAdamsRK4(RK4NegTable, A_radCurv, B_shapeFact, -1.0)
|
|
|
|
return (fTotalDiffSquared/(len(_PosEdgePoints) + len(_NegEdgePoints)))**0.5
|
|
|
|
|
|
optResult = scipy.optimize.minimize(msd, [_AGuess_radCurv, _BGuess_shapeFact, _XGuess, _TGuess_angle])
|
|
if optResult.success:
|
|
print("minimization success")
|
|
#print(optResult.x)
|
|
#print(optResult.fun)
|
|
return [float(optResult.fun)] + optResult.x.tolist()
|
|
else:
|
|
return [9999999,0,0,0,0]
|
|
|
|
|
|
# ========== Juza's equation for 1/H ==========
|
|
|
|
def HJuza(_S_diamRatio, _K):
|
|
# LowerS, UpperS, Ke, Kex, Kep, K3, K2, K1, K0
|
|
|
|
# 0.82 - 0.999
|
|
Coefficient8 = [[ 0.82, 0.83, 1791452.8, 533.85850, 170.55837, -81676.15812, 202187.89690, -166836.27680, 45888.22973],
|
|
[ 0.83, 0.84, 189.07578, 272.52455, 72.730022, -19963.60152, 50019.79936, -41775.38186, 11629.85610],
|
|
[ 0.84, 0.85, 7.1549709, 165.45945, 35.285687, -6944.66848, 17609.43832, -14883.84379, 4193.34232],
|
|
[ 0.85, 0.87, 1.1496269, 95.066407, 12.600013, -2158.91585, 5571.60176, -4792.82331, 1374.26272],
|
|
[ 0.87, 0.895, 0.47873040, 50.357240, 0.089787883, -567.76534, 1503.51828, -1327.11835, 390.45562],
|
|
[ 0.895, 0.93, 0.35255000, 25.498893, -5.4176608, -165.99710, 454.58851, -414.93772, 126.23908],
|
|
[ 0.93, 0.97, 0.32002037, 6.8474560, -8.0901766, -102.84970, 293.25377, -278.69176, 88.27639],
|
|
[ 0.97, 0.99, 0.30845061, -32.343947, -10.455428, -475.69091, 1398.86173, -1371.17931, 448.00538],
|
|
[ 0.99, 0.999, 0.30110729, -333.50440, -15.711260, -11334.69334, 33822.93507, -33642.61426, 11154.37157]]
|
|
|
|
# 0.635 - 0.997
|
|
Coefficient9 = [[ 0.635, 0.645, 58249804, 119.64583, 89.483167, -28072.11478, 53918.28040, -34519.94034, 7366.77050],
|
|
[ 0.645, 0.66, 5100.2910, 70.920100, 46.811007, -9051.10879, 17726.62108, -15572.21470, 2518.10214],
|
|
[ 0.66, 0.685, 19.518159, 38.509198, 19.951285, -2338.00251, 4719.64936, -3175.58038, 712.17229],
|
|
[ 0.685, 0.72, 1.3823760, 20.055606, 5.9746092, -522.67397, 1102.26575, -774.75596, 181.49582],
|
|
[ 0.72, 0.77, 0.49074291, 10.484929, -0.31885445, -111.99730, 250.54286, -186.78287, 46.40581],
|
|
[ 0.77, 0.84, 0.34607161, 5.4063548, -2.9788620, -24.21100, 58.53312, -47.15244, 12.65668],
|
|
[ 0.84, 0.93, 0.31342828, 2.1140055, -4.1151543, -9.16969, 24.37544, -21.58758, 6.36954],
|
|
[ 0.93, 0.98, 0.30397966, -3.6334200, -4.9395699, -29.80626, 85.43510, -81.61766, 25.98669],
|
|
[ 0.98, 0.997, 0.30007321, -34.095653, -6.1531194, -434.11439, 1287.65238, -1273.10762, 419.56923]]
|
|
|
|
# 0.17 - 0.983
|
|
Coefficient10 = [[ 0.17, 0.30, 0.31081678, -0.086278355, -2.7023254, -13.95071, 10.30398, -2.49619, 0.19805],
|
|
[ 0.30, 0.45, 0.30636442, -0.094871613, -2.7246428, 0.38766, -0.44128, 0.16613, -0.02068],
|
|
[ 0.45, 0.68, 0.31156188, -0.063734909, -2.6789763, 0.41537, -0.71168, 0.40321, -0.07551],
|
|
[ 0.68, 0.90, 0.31195754, -0.092720991, -2.6863859, -0.44813, 1.06637, -0.84260, 0.22106],
|
|
[ 0.90, 0.983, 0.30712046, -1.5619311, -2.9809169, -5.74538, 16.23781, -15.29128, 4.79808]]
|
|
|
|
# 0.06 - 0.953
|
|
Coefficient11 = [[ 0.06, 0.08, 1.6030433, 0.043380701, -0.15208454, -1341.32950, 282.27311, -19.71010, 0.45664],
|
|
[ 0.08, 0.13, 0.85491410, -0.056761738, -0.65372414, -160.97221, 50.59168, -5.23625, 0.17842],
|
|
[ 0.13, 0.20, 0.57401485, -0.15265815, -1.0443069, -42.79943, 21.22139, -3.47357, 0.18765],
|
|
[ 0.20, 0.30, 0.41622542, -0.27871823, -1.4464670, -14.08741, 10.58863, -2.63159, 0.21621],
|
|
[ 0.30, 0.44, 0.33587066, -0.42674255, -1.8022482, -3.22540, 3.58964, -1.32194, 0.16106],
|
|
[ 0.44, 0.75, 0.31504207, -0.51197565, -1.9499037, 0.07609, -0.13681, 0.08090, -0.01572],
|
|
[ 0.75, 0.953, 0.31658938, -0.49808609, -1.9290368, -0.24018, 0.61450, -0.52258, 0.14771]]
|
|
|
|
# 0.10 - 0.902
|
|
Coefficient12 = [[ 0.10, 0.12, 103.56308, 1.2019976, 4.4869062, -6625.95443, 2292.46751, -264.28560, 10.15218],
|
|
[ 0.12, 0.14, 2.1585254, 0.34129627, 0.83653215, -742.15388, 289.52312, -37.59979, 1.62555],
|
|
[ 0.14, 0.19, 0.65654048, 0.031877859, -0.37693028, -66.92134, 33.18430, -5.45788, 0.29773],
|
|
[ 0.19, 0.27, 0.45076908, -0.10378254, -0.82836350, -10.66154, 7.36263, -1.68453, 0.12768],
|
|
[ 0.27, 0.40, 0.37085833, -0.21965481, -1.1287016, -2.87493, 2.88810, -0.95966, 0.10546],
|
|
[ 0.40, 0.55, 0.33605594, -0.33317467, -1.3397591, -0.70394, 1.00402, -0.47492, 0.07450],
|
|
[ 0.55, 0.902, 0.32790337, -0.39597877, -1.4180887, 0.00490, -0.01070, 0.00769, -0.00182]]
|
|
|
|
if ( _K == 0.8):
|
|
Coeff = Coefficient8
|
|
if ( _K == 0.9):
|
|
Coeff = Coefficient9
|
|
if ( _K == 1.0):
|
|
Coeff = Coefficient10
|
|
if ( _K == 1.1):
|
|
Coeff = Coefficient11
|
|
if ( _K == 1.2):
|
|
Coeff = Coefficient12
|
|
|
|
# Juza 1996/1997 Equation: 8
|
|
for Entry_k in Coeff:
|
|
if (Entry_k[0] <= _S_diamRatio) and (_S_diamRatio <= Entry_k[1]):
|
|
HInv = Entry_k[2] * _S_diamRatio**(Entry_k[3] * math.log(_S_diamRatio) + Entry_k[4]) + Entry_k[5] * (_S_diamRatio**3.0) + Entry_k[6] * (_S_diamRatio **2.0) + Entry_k[7] * _S_diamRatio + Entry_k[8]
|
|
return HInv
|
|
break
|
|
|
|
return 0
|
|
|
|
|
|
|
|
# ========== USAGE PRINTER ==========
|
|
|
|
def PrintCorrectUsage():
|
|
print("\nDESCRIPTION:\n")
|
|
print("This program will take an image or a set of files (coordinate points) representing")
|
|
print("a droplet and will process the data to calculate the surface tension of the droplet.")
|
|
print("When an image file is set, the program will extract the coordinate of the points")
|
|
print("representing the edge of the droplet. When the files are set, it is assume that they")
|
|
print("will already contain the points representing the edge of the droplet. RK4 will be")
|
|
print("use to solve the Bashforth-Adams set of equations to get the parameters for surface")
|
|
print("tension calculation. parameters: A (apex radius curvature) and B (shape factor)")
|
|
print("\nUSAGE:\n")
|
|
print("Syntax 1: ./img.py -option1 value1 -option2 value2")
|
|
print("Syntax 2: python img.py -option1 value1 -option2 value2\n")
|
|
print("Options:")
|
|
print("-help : Prints this help message. ")
|
|
print("-out : Reference name for all the output files (coordinate files, RK4 dump, etc.).")
|
|
print(" : This should always be set. ")
|
|
print("-img : Image file to be processed. ")
|
|
print(" : When this is set, the file name inputs (-filep, -filen) should not be set.")
|
|
print("-filep : File containing the positive(x) point coordinates to be processed.")
|
|
print(" : If this is set, -filen should also be set and -img should not be set.")
|
|
print("-filen : File containing the negative(x) point coordinates to be processed.")
|
|
print(" : If this is set, -filep should also be set and -img should not be set.")
|
|
print("-proc : The number of processor to use.")
|
|
print(" : If not specified the default value is 2.")
|
|
print("-syrg : This is the syringe width in millimeters.")
|
|
print(" : If not specified the default value is 0.805 mm.")
|
|
print("-a : RK4 Parameter: minimum value for alpha (radius of curvature at apex).")
|
|
print(" : If not specified the default value is 0.0001")
|
|
print("-A : RK4 Parameter: maximum value for alpha (radius of curvature at apex).")
|
|
print(" : If not specified the default value is 2.0001")
|
|
print("-b : RK4 Parameter: minimum value for beta (shape factor).")
|
|
print(" : If not specified the default value is 0.0001")
|
|
print("-B : RK4 Parameter: minimum value for beta (shape factor).")
|
|
print(" : If not specified the default value is 0.5001.")
|
|
print("-x : RK4 Parameter: minimum value for x (initial guessed x coordinate).")
|
|
print(" : If not specified the default value is 0.0001.")
|
|
print("-X : RK4 Parameter: maximum value for x (initial guessed x coordinate).")
|
|
print(" : If not specified the default value is 0.5001.")
|
|
print("-t : RK4 Parameter: minimum value for t (initial guessed angle).")
|
|
print(" : If not specified the default value is 0.0001.")
|
|
print("-T : RK4 Parameter: maximum value for t (initial guessed angle).")
|
|
print(" : If not specified the default value is 0.5001.")
|
|
print("-ai : RK4 Parameter: alpha interval - the number of values to consider between min alpha and max alpha.")
|
|
print(" : If not specified the default value is 20.")
|
|
print("-bi : RK4 Parameter: beta interval - the number of values to consider between min beta and max beta.")
|
|
print(" : If not specified the default value is 20.")
|
|
print("-xi : RK4 Parameter: x interval - the number of values to consider between min x and max x.")
|
|
print(" : If not specified the default value is 5.")
|
|
print("-ti : RK4 Parameter: angle interval - the number of values to consider between min angle and max angle.")
|
|
print(" : If not specified the default value is 5.")
|
|
print("-solver : 1 for divide and conquer scanning.")
|
|
print(" : If not specified, optimization will be used to solve the parameters.")
|
|
print("\nSAMPLE USAGE:\n")
|
|
print("1. Calculate surface tension of a drop using default values.")
|
|
print("./img.py -img water-drop.jpg -out water-drop")
|
|
print("python img.py -img water-drop.jpg -out water-drop\n")
|
|
print("2. Uses 4 processors for calculation.")
|
|
print("./img.py -img water-drop.jpg -out water-drop -proc 4\n")
|
|
print("3. Setting interval sizes for A and B parameters (search for 30 possible values for A and B).")
|
|
print("./img.py -img water-drop.jpg -out water-drop -ai 30 -bi 30\n")
|
|
print("4. Setting min (0.00001) and max (1.5) values for parameter.")
|
|
print("./img.py -img water-drop.jpg -out water-drop -a 0.00001 -A 1.5\n")
|
|
|
|
|
|
|
|
# ========== MAIN ==========
|
|
|
|
if __name__ == '__main__':
|
|
|
|
ImageFilePath = ""
|
|
OutputFileName = ""
|
|
PosPointsFileName = ""
|
|
NegPointsFileName = ""
|
|
ProcessCount = 2
|
|
SyringeWidth = 0.70 * 1.15
|
|
MinA_radCurv = 0.0001
|
|
MaxA_radCurv = 2.0001
|
|
MinB_shapeFact = 0.0001
|
|
MaxB_shapeFact = 1.0001
|
|
MinX = 0.0001
|
|
MaxX = 0.5001
|
|
MinT_angle = 0.0001
|
|
MaxT_angle = 0.5001
|
|
AStep = 20
|
|
BStep = 20
|
|
XStep = 5
|
|
TStep = 5
|
|
|
|
if (len(sys.argv) <= 1):
|
|
PrintCorrectUsage()
|
|
sys.exit()
|
|
|
|
ImageFlag = 0
|
|
PosFileFlag = 0
|
|
NegFileFlag = 0
|
|
OutFlag = 0
|
|
Counter = 1
|
|
SolveMethod = 0
|
|
|
|
while (Counter <= len(sys.argv)-1):
|
|
|
|
CurrentOption = sys.argv[Counter]
|
|
try:
|
|
CurrentValue = sys.argv[Counter+1]
|
|
except:
|
|
PrintCorrectUsage()
|
|
sys.exit()
|
|
|
|
if CurrentOption == "-help":
|
|
PrintCorrectUsage()
|
|
sys.exit()
|
|
elif CurrentOption == "-img":
|
|
ImageFilePath = CurrentValue
|
|
ImageFlag = 1
|
|
elif CurrentOption == "-filep":
|
|
PosPointsFileName = CurrentValue
|
|
PosFileFlag = 1
|
|
elif CurrentOption == "-filen":
|
|
NegPointsFileName = CurrentValue
|
|
NegFileFlag = 1
|
|
elif CurrentOption == "-out":
|
|
OutputFileName = CurrentValue
|
|
OutFlag = 1
|
|
elif CurrentOption == "-proc":
|
|
ProcessCount = int(CurrentValue)
|
|
elif CurrentOption == "-syrg":
|
|
SyringeWidth = float(CurrentValue)
|
|
elif CurrentOption == "-a":
|
|
MinA_radCurv = float(CurrentValue)
|
|
elif CurrentOption == "-A":
|
|
MaxA_radCurv = float(CurrentValue)
|
|
elif CurrentOption == "-b":
|
|
MinB_shapeFact = float(CurrentValue)
|
|
elif CurrentOption == "-B":
|
|
MaxB_shapeFact = float(CurrentValue)
|
|
elif CurrentOption == "-x":
|
|
MinX = float(CurrentValue)
|
|
elif CurrentOption == "-X":
|
|
MaxX = float(CurrentValue)
|
|
elif CurrentOption == "-t":
|
|
MinT_angle = float(CurrentValue)
|
|
elif CurrentOption == "-T":
|
|
MaxT_angle = float(CurrentValue)
|
|
elif CurrentOption == "-ai":
|
|
AStep = int(CurrentValue)
|
|
elif CurrentOption == "-bi":
|
|
BStep = int(CurrentValue)
|
|
elif CurrentOption == "-xi":
|
|
XStep = int(CurrentValue)
|
|
elif CurrentOption == "-ti":
|
|
TStep = int(CurrentValue)
|
|
elif CurrentOption == "-solver":
|
|
SolveMethod = int(CurrentValue)
|
|
else:
|
|
PrintCorrectUsage()
|
|
sys.exit()
|
|
Counter = Counter + 2
|
|
|
|
if (OutFlag == 0 and ImageFlag == 1):
|
|
print("Output file name not set. (-out)")
|
|
PrintCorrectUsage()
|
|
sys.exit()
|
|
if (ImageFlag == 1 and ((PosFileFlag == 1) or (NegFileFlag == 1))):
|
|
print("Image(-img) can not be set with files option (-filep, -filen).")
|
|
print("Either set image only or files only.")
|
|
PrintCorrectUsage()
|
|
sys.exit()
|
|
if (ImageFlag == 0 and ((PosFileFlag == 1 and NegFileFlag == 0) or (PosFileFlag == 0 and NegFileFlag == 1))):
|
|
print("File inputs (-filep, -filen) should both be set if image input is not set.")
|
|
PrintCorrectUsage()
|
|
sys.exit()
|
|
|
|
ARange_radCurv = []
|
|
a = MinA_radCurv
|
|
ahop = float(MaxA_radCurv - MinA_radCurv)/float(AStep-1)
|
|
while (a <= MaxA_radCurv):
|
|
ARange_radCurv.append(a)
|
|
a += ahop
|
|
if ahop == 0.0:
|
|
break
|
|
if(np.abs(MaxA_radCurv - ARange_radCurv[len(ARange_radCurv)-1] - ahop) < 0.000001 and ahop != 0.0):
|
|
ARange_radCurv.append(MaxA_radCurv)
|
|
|
|
BRange_shapeFact = []
|
|
b = MinB_shapeFact
|
|
bhop = float(MaxB_shapeFact - MinB_shapeFact)/float(BStep-1)
|
|
while (b <= MaxB_shapeFact):
|
|
BRange_shapeFact.append(b)
|
|
b += bhop
|
|
if bhop == 0:
|
|
break
|
|
if(np.abs(MaxB_shapeFact - BRange_shapeFact[len(BRange_shapeFact)-1] - bhop) < 0.000001 and bhop != 0.0):
|
|
BRange_shapeFact.append(MaxB_shapeFact)
|
|
|
|
XRange = []
|
|
x = MinX
|
|
xhop = float(MaxX - MinX)/float(XStep-1)
|
|
while (x <= MaxX):
|
|
XRange.append(x)
|
|
x += xhop
|
|
if xhop == 0:
|
|
break
|
|
if(np.abs(MaxX - XRange[len(XRange)-1] - xhop) < 0.000001 and xhop != 0.0):
|
|
XRange.append(MaxX)
|
|
|
|
TRange_angle = []
|
|
t = MinT_angle
|
|
thop = float(MaxT_angle - MinT_angle)/float(TStep-1)
|
|
while (t <= MaxT_angle):
|
|
TRange_angle.append(t)
|
|
t += thop
|
|
if thop == 0.0:
|
|
break
|
|
if(np.abs(MaxT_angle - TRange_angle[len(TRange_angle)-1] - thop) < 0.000001 and thop != 0.0):
|
|
TRange_angle.append(MaxT_angle)
|
|
|
|
|
|
print("\n")
|
|
if (ImageFlag == 1):
|
|
print("Image File : " + ImageFilePath)
|
|
print("Output Name : " + OutputFileName)
|
|
print("Negative Cooridnate File : " + OutputFileName + "_Negative.dat")
|
|
print("Positive Cooridnate File : " + OutputFileName + "_Positive.dat")
|
|
print("Syringe Width (millimeter): " + str(SyringeWidth))
|
|
else:
|
|
print("Negative Cooridnate File : " + NegPointsFileName)
|
|
print("Positive Cooridnate File : " + PosPointsFileName)
|
|
print("Radius of Curvature Range : " + str(MinA_radCurv) + "-" + str(MaxA_radCurv) + " (interval: "+str(len(ARange_radCurv))+")")
|
|
print("Shape Factor Range : " + str(MinB_shapeFact) + "-" + str(MaxB_shapeFact) + " (interval: "+str(len(BRange_shapeFact))+")")
|
|
print("Initial X Coordinate Range: " + str(MinX) + "-" + str(MaxX) + " (interval: "+str(len(XRange))+")")
|
|
print("Initial Angle Range : " + str(MinT_angle) + "-" + str(MaxT_angle) + " (interval: "+str(len(TRange_angle))+")")
|
|
print("Parameter Combinations : " + str(len(ARange_radCurv) * len(BRange_shapeFact) * len(XRange) * len(TRange_angle)))
|
|
print("Number of Process to Use : " + str(ProcessCount))
|
|
|
|
if (ImageFlag == 1):
|
|
Pos_edgePts, Neg_edgePts, De_scaledMaxWidth, Dms_diamters = ExtractEdgeCoordinate(ImageFilePath, OutputFileName, SyringeWidth)
|
|
HInv1 = HJuza(Dms_diamters[7]/De_scaledMaxWidth, 0.8)
|
|
HInv2 = HJuza(Dms_diamters[8]/De_scaledMaxWidth, 0.9)
|
|
HInv3 = HJuza(Dms_diamters[9]/De_scaledMaxWidth, 1.0)
|
|
HInv4 = HJuza(Dms_diamters[10]/De_scaledMaxWidth, 1.1)
|
|
HInv5 = HJuza(Dms_diamters[11]/De_scaledMaxWidth, 1.2)
|
|
HInvM = (0.20)*(HInv1 + HInv2 + HInv3 + HInv4 + HInv5)
|
|
HInvSD = math.sqrt((0.20)*( (HInv1-HInvM)**2.0 + (HInv2-HInvM)**2.0 + (HInv3-HInvM)**2.0 + (HInv4-HInvM)**2.0 + (HInv5-HInvM)**2.0))
|
|
print("Juza 5-Plane (1/H) Std.Dev: " + str(HInvSD))
|
|
else:
|
|
Pos_edgePts = []
|
|
PositivePointFile = open(PosPointsFileName,'r')
|
|
for Line in PositivePointFile:
|
|
Coordinate = Line.split()
|
|
#print(Coordinate)
|
|
Pos_edgePts.append((float(Coordinate[0]), float(Coordinate[1])))
|
|
PositivePointFile.close()
|
|
Neg_edgePts = []
|
|
NegativePointFile = open(NegPointsFileName,'r')
|
|
for Line in NegativePointFile:
|
|
Coordinate = Line.split()
|
|
#print(Coordinate)
|
|
Neg_edgePts.append((float(Coordinate[0]), float(Coordinate[1])))
|
|
NegativePointFile.close()
|
|
|
|
def BruteForce():
|
|
PosList = []
|
|
NegList = []
|
|
for ProcessNo in range(ProcessCount):
|
|
PosList.append(copy.deepcopy(Pos_edgePts))
|
|
NegList.append(copy.deepcopy(Neg_edgePts))
|
|
#print(ProcessNo)
|
|
|
|
ARanges = []
|
|
for ProcessNo in range(ProcessCount):
|
|
ARanges.append([])
|
|
|
|
i = 0
|
|
for A in ARange_radCurv:
|
|
ARanges[i%ProcessCount].append(A)
|
|
i += 1
|
|
|
|
BestParams = multiprocessing.Queue()
|
|
TicksQueue = multiprocessing.Queue()
|
|
Processes = []
|
|
|
|
for ProcessNo in range(ProcessCount):
|
|
Processes.append(multiprocessing.Process(name='Search ' + str(ProcessNo), target=FindOptimalParameters, args=(PosList[ProcessNo], NegList[ProcessNo], ARanges[ProcessNo], BRange_shapeFact, XRange, TRange_angle, BestParams, TicksQueue)))
|
|
|
|
for aProcess in Processes:
|
|
aProcess.start()
|
|
|
|
sys.stdout.write("\rParameter Search Progress : 0%")
|
|
sys.stdout.flush()
|
|
Count = 0
|
|
while(Count < len(ARange_radCurv)):
|
|
t = TicksQueue.get()
|
|
Count += 1
|
|
sys.stdout.write("\rParameter Search Progress : " + str(int(100*float(Count)/float(len(ARange_radCurv)))) + "%")
|
|
sys.stdout.flush()
|
|
|
|
for aProcess in Processes:
|
|
aProcess.join()
|
|
|
|
BestOutput = (999999999, 1, 1, 1 ,1)
|
|
while not BestParams.empty():
|
|
Output = BestParams.get()
|
|
if Output[0] < BestOutput[0]:
|
|
BestOutput = Output
|
|
return BestOutput
|
|
|
|
def Optimize():
|
|
return OptimizeParameters(Pos_edgePts, Neg_edgePts, 0.95, 0.3, 0.1, 0.2)
|
|
|
|
starttime = time.time()
|
|
|
|
BestOutput = Optimize() if 1 != SolveMethod else BruteForce()
|
|
|
|
print("best output: " + str(BestOutput))
|
|
|
|
print("\nLowest RMSD : " + str(BestOutput[0]))
|
|
print("Computed Surface Tension : " + str(9.8*BestOutput[1]/BestOutput[2]))
|
|
print("Best Radius of Curvature : " + str(BestOutput[1]))
|
|
print("Best Shape Factor : " + str(BestOutput[2]))
|
|
print("Best Initial X Coordinate : " + str(BestOutput[3]))
|
|
print("Best Initial Angle : " + str(BestOutput[4]))
|
|
print("Lapsed Time in Seconds : " + str(time.time() -starttime))
|