327 lines
11 KiB
Python
Raw Normal View History

2025-07-13 08:55:18 +08:00
#!/usr/bin/python
# coding: UTF-8
#
# Author: Dawid Laszuk
# Contact: https://github.com/laszukdawid/PyEMD/issues
#
# Feel free to contact for any information.
import logging
import numpy as np
from scipy.interpolate import Rbf
try:
from skimage.morphology import reconstruction
except (ImportError, ModuleNotFoundError):
raise ImportError(
"EMD2D and BEMD are not supported. Feel free to play around and improve them. "
+ "Required dependencies are in `requirements-extra`."
)
class BEMD:
"""
**Bidimensional Empirical Mode Decomposition**
**Important**: This class intends to be undocumented until it's actually properly tested
and proven to work. An attempt to replicate findings in the paper cited below has failed.
This method is only included in the package because someone asked for it, and I'm hoping
that one day someone else will come and *fix it*. Until then, USE AT YOUR OWN RISK.
The guess why the decomposition doesn't work is that it's difficult to extrapolate image
far away from extrema. Not even mirroring helps in this case.
Method decomposition 2D arrays like gray-scale images into 2D representations of
Intrinsic Mode Functions (IMFs).
The algorithm is based on Nunes et. al. [Nunes2003]_ work.
.. [Nunes2003] J.-C. Nunes, Y. Bouaoune, E. Delechelle, O. Niang, P. Bunel.,
"Image analysis by bidimensional empirical mode decomposition. Image and Vision Computing",
Elsevier, 2003, 21 (12), pp.1019-1026.
"""
logger = logging.getLogger(__name__)
def __init__(self):
# ProtoIMF related
self.mse_thr = 0.01
self.mean_thr = 0.01
self.FIXE = 1 # Single iteration by default, otherwise results are terrible
self.FIXE_H = 0
self.MAX_ITERATION = 5
def __call__(self, image, max_imf=-1):
return self.bemd(image, max_imf=max_imf)
def extract_max_min_spline(self, image, min_peaks_pos, max_peaks_pos):
"""Calculates top and bottom envelopes for image.
Parameters
----------
image : numpy 2D array
Returns
-------
min_env : numpy 2D array
Bottom envelope in form of an image.
max_env : numpy 2D array
Top envelope in form of an image.
"""
xi, yi = np.meshgrid(np.arange(image.shape[0]), np.arange(image.shape[1]))
min_val = np.array([image[x, y] for x, y in zip(*min_peaks_pos)])
max_val = np.array([image[x, y] for x, y in zip(*max_peaks_pos)])
min_env = self.spline_points(min_peaks_pos[0], min_peaks_pos[1], min_val, xi, yi)
max_env = self.spline_points(max_peaks_pos[0], max_peaks_pos[1], max_val, xi, yi)
return min_env, max_env
@classmethod
def spline_points(cls, X, Y, Z, xi, yi):
"""Creates a spline for given set of points.
Uses Radial-basis function to extrapolate surfaces. It's not the best but gives something.
Grid data algorithm didn't work.
"""
spline = Rbf(X, Y, Z, function="cubic")
return spline(xi, yi)
@classmethod
def find_extrema_positions(cls, image):
"""
Finds extrema, both minima and maxima, based on morphological reconstruction.
Returns extrema where the first and second elements are x and y positions, respectively.
Parameters
----------
image : numpy 2D array
Monochromatic image or any 2D array.
Returns
-------
min_peaks_pos : numpy array
Minima positions.
max_peaks_pos : numpy array
Maxima positions.
"""
min_peaks_pos = BEMD.extract_minima_positions(image)
max_peaks_pos = BEMD.extract_maxima_positions(image)
return min_peaks_pos, max_peaks_pos
@classmethod
def extract_minima_positions(cls, image):
return BEMD.extract_maxima_positions(-image)
@classmethod
def extract_maxima_positions(cls, image):
seed_min = image - 1
dilated = reconstruction(seed_min, image, method="dilation")
cleaned_image = image - dilated
return np.where(cleaned_image > 0)[::-1]
@classmethod
def end_condition(cls, image, IMFs):
"""Determines whether decomposition should be stopped.
Parameters
----------
image : numpy 2D array
Input image which is decomposed.
IMFs : numpy 3D array
Array for which first dimensions relates to respective IMF,
i.e. (numIMFs, imageX, imageY).
"""
rec = np.sum(IMFs, axis=0)
# If reconstruction is perfect, no need for more tests
if np.allclose(image, rec):
return True
return False
def check_proto_imf(self, proto_imf, proto_imf_prev, mean_env):
"""Check whether passed (proto) IMF is actual IMF.
Current condition is solely based on checking whether the mean is below threshold.
Parameters
----------
proto_imf : numpy 2D array
Current iteration of proto IMF.
proto_imf_prev : numpy 2D array
Previous iteration of proto IMF.
mean_env : numpy 2D array
Local mean computed from top and bottom envelopes.
Returns
-------
boolean
Whether current proto IMF is actual IMF.
"""
# TODO: Sifting is very sensitive and subtracting const val can often flip
# maxima with minima in decomposition and thus repeating above/below
# behaviour. For now, mean_env is checked whether close to zero excluding
# its offset.
if np.all(np.abs(mean_env - mean_env.mean()) < self.mean_thr):
# if np.all(np.abs(mean_env)<self.mean_thr):
return True
# If very little change with sifting
if np.allclose(proto_imf, proto_imf_prev, rtol=0.01):
return True
# If IMF mean close to zero (below threshold)
if np.mean(np.abs(proto_imf)) < self.mean_thr:
return True
# Everything relatively close to 0
mse_proto_imf = np.mean(proto_imf * proto_imf)
if mse_proto_imf > self.mse_thr:
return False
return False
def bemd(self, image, max_imf=-1):
"""Performs bidimensional EMD (BEMD) on grey-scale image with specified parameters.
Parameters
----------
image : numpy 2D array,
Grey-scale image.
max_imf : int, (default: -1)
IMF number to which decomposition should be performed.
Negative value means *all*.
Returns
-------
IMFs : numpy 3D array
Set of IMFs in form of numpy array where the first dimension
relates to IMF's ordinary number.
"""
image_s = image.copy()
imf = np.zeros(image.shape)
imf_old = imf.copy()
imfNo = 0
IMF = np.empty((imfNo,) + image.shape)
notFinished = True
while notFinished:
self.logger.debug("IMF -- " + str(imfNo))
res = image_s - np.sum(IMF[:imfNo], axis=0)
imf = res.copy()
mean_env = np.zeros(image.shape)
stop_sifting = False
# Counters
n = 0 # All iterations for current imf.
n_h = 0 # counts when mean(proto_imf) < threshold
while not stop_sifting and n < self.MAX_ITERATION:
n += 1
self.logger.debug("Iteration: %i", n)
min_peaks_pos, max_peaks_pos = self.find_extrema_positions(imf)
self.logger.debug(
"min_peaks_pos = %i | max_peaks_pos = %i", len(min_peaks_pos[0]), len(max_peaks_pos[0])
)
if len(min_peaks_pos[0]) > 1 and len(max_peaks_pos[0]) > 1:
min_env, max_env = self.extract_max_min_spline(imf, min_peaks_pos, max_peaks_pos)
mean_env = 0.5 * (min_env + max_env)
imf_old = imf.copy()
imf = imf - mean_env
# Fix number of iterations
if self.FIXE:
if n >= self.FIXE + 1:
stop_sifting = True
# Fix number of iterations after number of zero-crossings
# and extrema differ at most by one.
elif self.FIXE_H:
if n == 1:
continue
if self.check_proto_imf(imf, imf_old, mean_env):
n_h += 1
else:
n_h = 0
# STOP if enough n_h
if n_h >= self.FIXE_H:
stop_sifting = True
# Stops after default stopping criteria are met
else:
if self.check_proto_imf(imf, imf_old, mean_env):
stop_sifting = True
else:
stop_sifting = True
IMF = np.vstack((IMF, imf.copy()[None, :]))
imfNo += 1
if self.end_condition(image, IMF) or (max_imf > 0 and imfNo >= max_imf):
notFinished = False
break
res = image_s - np.sum(IMF[:imfNo], axis=0)
if not np.allclose(res, 0):
IMF = np.vstack((IMF, res[None, :]))
imfNo += 1
return IMF
if __name__ == "__main__":
print("Running example on BEMD")
PLOT = True
logging.basicConfig(level=logging.DEBUG)
# Generate image
print("Generating image... ", end="")
rows, cols = 256, 256
row_scale, col_scale = 256, 256
x = np.arange(rows) / float(row_scale)
y = np.arange(cols).reshape((-1, 1)) / float(col_scale)
pi2 = 2 * np.pi
img = np.zeros((rows, cols))
# img = img + np.sin(2*pi2*x)*np.cos(y*4*pi2+4*x*pi2)
img = img + 3 * np.sin(2 * pi2 * x) + 2
# img = img + 5*x*y + 2*(y-0.2)*y
print("Done")
# Perform decomposition
print("Performing decomposition... ", end="")
bemd = BEMD()
# bemd.FIXE_H = 5
IMFs = bemd.bemd(img, max_imf=3)
imfNo = IMFs.shape[0]
print("Done")
if PLOT:
print("Plotting results... ", end="")
import pylab as plt
# Save image for preview
plt.figure(figsize=(4, 4 * (imfNo + 1)))
plt.subplot(imfNo + 1, 1, 1)
plt.imshow(img)
plt.colorbar()
plt.title("Input image")
# Save reconstruction
for n, imf in enumerate(IMFs):
plt.subplot(imfNo + 1, 1, n + 2)
plt.imshow(imf)
plt.colorbar()
plt.title("IMF %i" % (n + 1))
plt.savefig("image_decomp")
print("Done")