DigitalGrainSize / funcs.py
dbuscombe's picture
v1
d5f12de
# Written by Dr Daniel Buscombe, Marda Science LLC
#
# MIT License
#
# Copyright (c) 2022, Marda Science LLC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# from imageio import imread
import pywt
#from tqdm import tqdm
from skimage.restoration import denoise_wavelet, estimate_sigma
from functools import partial
# rescale_sigma=True required to silence deprecation warnings
_denoise_wavelet = partial(denoise_wavelet, rescale_sigma=True)
import numpy as np
import scipy.stats as stats
from glob import glob
def rescale(dat,mn,mx):
"""
rescales an input dat between mn and mx
"""
m = min(dat.flatten())
M = max(dat.flatten())
return (mx-mn)*(dat-m)/(M-m)+mn
##====================================
def standardize(img):
img = np.array(img)
#standardization using adjusted standard deviation
N = np.shape(img)[0] * np.shape(img)[1]
s = np.maximum(np.std(img), 1.0/np.sqrt(N))
m = np.mean(img)
img = (img - m) / s
img = rescale(img, 0, 1)
del m, s, N
return img
# =========================================================
# =========================================================
def dgs(input_img, resolution=1, maxscale=4, verbose=1, x=-0.5):
#if verbose==1:
print("===========================================")
print("======DIGITAL GRAIN SIZE: WAVELET==========")
print("===========================================")
print("=CALCULATE GRAIN SIZE-DISTRIBUTION FROM AN=")
print("====IMAGE OF SEDIMENT/GRANULAR MATERIAL====")
print("===========================================")
print("======A PROGRAM BY DANIEL BUSCOMBE=========")
print("====MARDASCIENCE, FLAGSTAFF, ARIZONA=======")
print("========REVISION 4.2, APR 2022===========")
print("===========================================")
# ======= stage 1 ==========================
#read image
if verbose==1:
print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
print('Processing image ')
im = np.squeeze(input_img) # squeeze singleton dimensions
if len(np.shape(im))>3:
im = im[:, :, :3] # only keep the first 3 bands
if len(np.shape(im))==3: # if rgb, convert to grey
im = (0.299 * im[:,:,0] + 0.5870*im[:,:,1] + 0.114*im[:,:,2]).astype('uint8')
nx,ny = np.shape(im)
if nx>ny:
im=im.T
im = standardize(im)
# # ======= stage 2 ==========================
# Denoised image using default parameters of `denoise_wavelet`
filter=False
if filter:
sigma_est = estimate_sigma(im, multichannel=False, average_sigmas=True)
region = denoise_wavelet(im, multichannel=False, rescale_sigma=True,
method='VisuShrink', mode='soft', sigma=sigma_est)
else:
region = im.copy()
original = rescale(region,0,255)
nx, ny = original.shape
# ======= stage 3 ==========================
# call cwt to get particle size distribution
## initial guess
P = []; M = []
for k in np.linspace(1,nx-1,40):
[cfs, frequencies] = pywt.cwt(original[int(k),:], np.arange(3, np.maximum(nx,ny)/maxscale, 1), 'morl' , .5)
period = 1. / frequencies
power =(abs(cfs)) ** 2
power = np.mean(np.abs(power), axis=1)/(period**2)
P.append(power)
M.append(period[np.argmax(power)])
p = np.mean(np.vstack(P), axis=0)
p = np.array(p/np.sum(p))
# get real scales by multiplying by resolution (mm/pixel)
scales = np.array(period)*resolution
print(np.sum(p*scales))
if np.sum(p*scales)>80:
x=1
maxscale=4
elif (np.sum(p*scales)<80) and (np.sum(p*scales)>60):
x=0.75
maxscale=8
elif (np.sum(p*scales)<60) and (np.sum(p*scales)>40):
x=0.5
maxscale=12
elif (np.sum(p*scales)<40) and (np.sum(p*scales)>20):
x=-0.5
maxscale=16
elif np.sum(p*scales)<20:
x=-1
maxscale=20
print("x is {}".format(x))
print("maxscale is {}".format(maxscale))
## for real
P = []; M = []
for k in np.linspace(1,nx-1,100):
[cfs, frequencies] = pywt.cwt(original[int(k),:], np.arange(3, np.maximum(nx,ny)/maxscale, 1), 'morl' , .5)
period = 1. / frequencies
power =(abs(cfs)) ** 2
power = np.mean(np.abs(power), axis=1)/(period**2)
P.append(power)
M.append(period[np.argmax(power)])
p = np.mean(np.vstack(P), axis=0)
p = np.array(p/np.sum(p))
# get real scales by multiplying by resolution (mm/pixel)
scales = np.array(period)*resolution
srt = np.sqrt(np.sum(p*((scales-np.mean(M))**2)))
# plt.plot(scales, p,'m', lw=2)
p = p+stats.norm.pdf(scales, np.mean(M), srt/np.pi)
p = p/np.sum(p)
mnsz = np.sum(p*scales)
srt = np.sqrt(np.sum(p*((scales-mnsz)**2)))
ind =np.where(scales < (mnsz+3*srt))[0]
scales= scales[ind]
p = p[ind]
# p = np.hstack([0,p])
# scales = np.hstack([0,scales])
# area-by-number to volume-by-number
r_v = (p*scales**x) / np.sum(p*scales**x) #volume-by-weight proportion
# ======= stage 5 ==========================
# calc particle size stats
pd = np.interp([.05,.1,.16,.25,.3,.5,.75,.84,.9,.95],np.hstack((0,np.cumsum(r_v))), np.hstack((0,scales)) )
if verbose==1:
print("d50 = "+str(pd[4]))
mnsz = np.sum(r_v*scales)
if verbose==1:
print("mean size = "+str(mnsz))
srt = np.sqrt(np.sum(r_v*((scales-mnsz)**2)))
if verbose==1:
print("stdev = "+str(srt))
sk = (sum(r_v*((scales-mnsz)**3)))/(100*srt**3)
if verbose==1:
print("skewness = "+str(sk))
kurt = (sum(r_v*((scales-mnsz)**4)))/(100*srt**4)
if verbose==1:
print("kurtosis = "+str(kurt))
# ======= stage 6 ==========================
# return a dict object of stats
return {'mean grain size': mnsz, 'grain size sorting': srt, 'grain size skewness': sk, 'grain size kurtosis': kurt, 'percentiles': [.05,.1,.16,.25,.3,.5,.75,.84,.9,.95], 'percentile_values': pd, 'grain size frequencies': r_v, 'grain size bins': scales}