PySDKit 0.4.38__tar.gz → 0.4.39__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pysdkit-0.4.38 → pysdkit-0.4.39}/PKG-INFO +1 -1
- {pysdkit-0.4.38 → pysdkit-0.4.39}/PySDKit.egg-info/PKG-INFO +1 -1
- {pysdkit-0.4.38 → pysdkit-0.4.39}/PySDKit.egg-info/SOURCES.txt +1 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/__init__.py +1 -1
- pysdkit-0.4.39/pysdkit/_vmd2d/cvmd2d.py +410 -0
- pysdkit-0.4.39/pysdkit/tests/test_cvmd2d.py +158 -0
- pysdkit-0.4.38/pysdkit/_vmd2d/cvmd2d.py +0 -471
- {pysdkit-0.4.38 → pysdkit-0.4.39}/LICENSE +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/PySDKit.egg-info/dependency_links.txt +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/PySDKit.egg-info/requires.txt +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/PySDKit.egg-info/top_level.txt +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/README.md +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pyproject.toml +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_alif/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_alif/alif.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_alif/data/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_alif/data/prefixed_double_filter.npy +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_alif/iterative_filtering.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_apmd/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_apmd/apmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_emd/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_emd/_find_extrema.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_emd/_prepare_points.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_emd/_splines.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_emd/ceemdan.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_emd/eemd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_emd/efd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_emd/emd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_emd/hht/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_emd/hht/frequency.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_emd/hht/hht.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_emd/memd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_emd/remd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_emd/semd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_emd/tvf_emd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_emd2d/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_emd2d/bemd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_emd2d/bmemd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_emd2d/emd2d.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_esmd/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_esmd/esmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_ewt/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_ewt/ewt.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_ewt/ewt2d.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_faemd/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_faemd/extrema.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_faemd/faemd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_faemd/faemd2d.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_faemd/faemd3d.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_faemd/filter.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_fmd/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_fmd/fmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_fmd/nfmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_gdmd/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_gdmd/gdmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_gdmd/vgnmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_hvd/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_hvd/hvd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_itd/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_itd/itd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_jmd/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_jmd/jmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_jmd/mjmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_jmd/sjmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_lmd/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_lmd/lmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_lmd/rlmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_osd/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_osd/swd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_ssa/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_ssa/ssa.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_vmd/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_vmd/acmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_vmd/avmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_vmd/ba_acmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_vmd/base.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_vmd/mvmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_vmd/ovmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_vmd/stvmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_vmd/svmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_vmd/vmd_c.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_vmd/vmd_f.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_vmd/vme.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_vmd2d/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_vmd2d/vmd2d.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_vncmd/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_vncmd/avncmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_vncmd/incmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/_vncmd/vncmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/data/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/data/_add_noise.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/data/_cube.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/data/_generator.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/data/_image.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/data/_models.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/data/_test_univariate.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/data/_time_series.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/data/texture.txt +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/entropy/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/entropy/_approxiamte_entropy.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/entropy/_permutation_entropy.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/entropy/_sample_entropy.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/models/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/models/_base.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/models/_kmeans.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/models/_knn.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/models/_pca.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/plot/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/plot/_fourier_spectra.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/plot/_functions.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/plot/_plot_images.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/plot/_plot_imfs.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/plot/_plot_signal.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/data/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/data/test_generator_cube.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/data/test_generator_image.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/data/test_generator_signal.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/data/test_generator_univariate_signal.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/models/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/models/test_knn.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/models/test_pca.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/run_all.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_alif.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_apmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_avncmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_bmemd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_ceemdan.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_eemd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_emd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_ewt.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_ewt2d.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_faemd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_faemd2d.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_faemd3d.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_gdmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_itd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_jmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_lmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_moving_decomp.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_mstl.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_ovmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_rlmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_semd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_sjmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_ssa.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_stl.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_stvmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_svmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_swd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_vgnmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_vmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_vmd2d.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tests/test_vncmd.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tsa/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tsa/_dtw.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tsa/_moving_decomp.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tsa/_mstl.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/tsa/_stl.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/utils/__init__.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/utils/_cite.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/utils/_correlation.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/utils/_diagnalization.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/utils/_differ.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/utils/_fft.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/utils/_function.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/utils/_hilbert.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/utils/_instantaneous.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/utils/_kernel_matrix.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/utils/_mirror.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/utils/_process.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/pysdkit/utils/_smooth1d.py +0 -0
- {pysdkit-0.4.38 → pysdkit-0.4.39}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PySDKit
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.39
|
|
4
4
|
Summary: A Python library for signal decomposition algorithms with a unified interface.
|
|
5
5
|
Author: josefinez, Deeksha Manjunath, Yuan Feng, JacktheFowler
|
|
6
6
|
Author-email: Whenxuan Wang <wwhenxuan@gmail.com>, RuiZhe Wang <3133986068@qq.com>, WenTong Zhao <23049200290@stu.xidian.edu.cn>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PySDKit
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.39
|
|
4
4
|
Summary: A Python library for signal decomposition algorithms with a unified interface.
|
|
5
5
|
Author: josefinez, Deeksha Manjunath, Yuan Feng, JacktheFowler
|
|
6
6
|
Author-email: Whenxuan Wang <wwhenxuan@gmail.com>, RuiZhe Wang <3133986068@qq.com>, WenTong Zhao <23049200290@stu.xidian.edu.cn>
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Created on 2025/02/12 11:06:23
|
|
4
|
+
@author: Whenxuan Wang
|
|
5
|
+
@email: wwhenxuan@gmail.com
|
|
6
|
+
|
|
7
|
+
Compact / Two-Dimensional TV Variational Mode Decomposition (CVMD2D / 2D-TV-VMD).
|
|
8
|
+
|
|
9
|
+
Zosso D., Dragomiretskiy K., Bertozzi A.L., Weiss P.S.
|
|
10
|
+
Two-Dimensional Compact Variational Mode Decomposition.
|
|
11
|
+
Journal of Mathematical Imaging and Vision, 58(2):294–320, 2017.
|
|
12
|
+
https://doi.org/10.1007/s10851-017-0710-z
|
|
13
|
+
|
|
14
|
+
MATLAB reference:
|
|
15
|
+
https://www.mathworks.com/matlabcentral/fileexchange/67285-two-dimensional-compact-variational-mode-decomposition-2d-tv-vmd
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Optional, Tuple, Union
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
from numpy.linalg import norm
|
|
23
|
+
|
|
24
|
+
from pysdkit.utils import fft2d, ifft2d, fftshift, ifftshift
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CVMD2D(object):
|
|
28
|
+
"""
|
|
29
|
+
Compact Variational Mode Decomposition for 2D Images (2D-TV-VMD).
|
|
30
|
+
|
|
31
|
+
Decomposes an image into spatially compact, spectrally sparse modes with
|
|
32
|
+
optional support segmentation (MBO / winner-takes-all) and artifact maps.
|
|
33
|
+
|
|
34
|
+
The optimisation proceeds in three scheduled phases (``A_phase = [a, b]``):
|
|
35
|
+
|
|
36
|
+
- iterations ``1 … a-1``: classical 2D VMD (no spatial support evolution)
|
|
37
|
+
- iterations ``a … b-1``: 2D-TV-VMD (individual MBO on supports ``A_k``)
|
|
38
|
+
- iterations ``b … end``: segmented 2D-TV-VMD (joint winner-takes-all)
|
|
39
|
+
|
|
40
|
+
MATLAB code: ``VMD_2D_TV.m`` (Zosso & Dragomiretskiy).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
K: int = 5,
|
|
46
|
+
alpha: float = 1000,
|
|
47
|
+
beta: float = 0.5,
|
|
48
|
+
gamma: float = 500,
|
|
49
|
+
delta: float = np.inf,
|
|
50
|
+
rho: float = 10,
|
|
51
|
+
rho_k: float = 10,
|
|
52
|
+
tau: float = 0.0,
|
|
53
|
+
tau_k: float = 2.5,
|
|
54
|
+
t: float = 1.5,
|
|
55
|
+
DC: bool = False,
|
|
56
|
+
init: Union[str, int, np.ndarray] = "radially",
|
|
57
|
+
u_tol: float = 1e-10,
|
|
58
|
+
A_tol: float = 1e-4,
|
|
59
|
+
omega_tol: float = 1e-10,
|
|
60
|
+
max_iter: int = 130,
|
|
61
|
+
M: int = 1,
|
|
62
|
+
A_phase: Optional[np.ndarray] = None,
|
|
63
|
+
random_seed: int = 42,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""
|
|
66
|
+
:param K: number of modes
|
|
67
|
+
:param alpha: spectral narrow-band / bandwidth penalty
|
|
68
|
+
:param beta: L1 area penalty on spatial supports ``A_k``
|
|
69
|
+
:param gamma: heat-diffusion weight for TV (MBO) propagation of ``A_k``
|
|
70
|
+
:param delta: artifact threshold on residual energy (``inf`` → disabled)
|
|
71
|
+
:param rho: data-fidelity weight
|
|
72
|
+
:param rho_k: u–v splitting weight
|
|
73
|
+
:param tau: dual step for data fidelity (0 → noise-slack)
|
|
74
|
+
:param tau_k: dual step for u–v splitting
|
|
75
|
+
:param t: ODE / PDE step for support updates
|
|
76
|
+
:param DC: keep the first mode at the DC frequency ``(0, 0)``
|
|
77
|
+
:param init: ``"radially"`` / ``"uniform"`` / ``0``, ``"random"`` / ``1``,
|
|
78
|
+
or an array of shape ``(2, K, M)`` with custom centre frequencies
|
|
79
|
+
:param u_tol: relative tolerance on modes ``u``
|
|
80
|
+
:param A_tol: absolute tolerance on supports ``A``
|
|
81
|
+
:param omega_tol: tolerance on centre frequencies
|
|
82
|
+
:param max_iter: maximum ADMM iterations ``N``
|
|
83
|
+
:param M: number of spectral sub-modes per spatial mode
|
|
84
|
+
:param A_phase: ``[a, b]`` phase schedule (default ``[100, 150]``)
|
|
85
|
+
:param random_seed: RNG seed for random omega initialisation
|
|
86
|
+
"""
|
|
87
|
+
self.K = int(K)
|
|
88
|
+
self.alpha = float(alpha)
|
|
89
|
+
self.beta = float(beta)
|
|
90
|
+
self.gamma = float(gamma)
|
|
91
|
+
self.delta = float(delta)
|
|
92
|
+
self.rho = float(rho)
|
|
93
|
+
self.rho_k = float(rho_k)
|
|
94
|
+
self.tau = float(tau)
|
|
95
|
+
self.tau_k = float(tau_k)
|
|
96
|
+
self.t = float(t)
|
|
97
|
+
self.DC = bool(DC)
|
|
98
|
+
self.init = init
|
|
99
|
+
self.u_tol = float(u_tol)
|
|
100
|
+
self.A_tol = float(A_tol)
|
|
101
|
+
self.omega_tol = float(omega_tol)
|
|
102
|
+
self.max_iter = int(max_iter)
|
|
103
|
+
self.M = int(M)
|
|
104
|
+
if A_phase is None:
|
|
105
|
+
self.A_phase = np.array([100.0, 150.0], dtype=float)
|
|
106
|
+
else:
|
|
107
|
+
self.A_phase = np.asarray(A_phase, dtype=float).ravel()
|
|
108
|
+
if self.A_phase.size != 2:
|
|
109
|
+
raise ValueError("A_phase must be a length-2 array [a, b]")
|
|
110
|
+
|
|
111
|
+
self.rng = np.random.default_rng(seed=random_seed)
|
|
112
|
+
|
|
113
|
+
def __call__(
|
|
114
|
+
self, image: np.ndarray, return_all: bool = False
|
|
115
|
+
) -> Union[
|
|
116
|
+
np.ndarray, Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]
|
|
117
|
+
]:
|
|
118
|
+
return self.fit_transform(image=image, return_all=return_all)
|
|
119
|
+
|
|
120
|
+
def __str__(self) -> str:
|
|
121
|
+
return "Compact Variational Mode Decomposition for 2D Images (CVMD2D)"
|
|
122
|
+
|
|
123
|
+
def _init_omega(self) -> np.ndarray:
|
|
124
|
+
"""Initialise centre frequencies ``omega`` (MATLAB ``init`` cases 0/1 / custom)."""
|
|
125
|
+
# (N+1) × 2 × K × M — extra slot so omega[n+1] is always valid
|
|
126
|
+
omega = np.zeros((self.max_iter + 1, 2, self.K, self.M), dtype=float)
|
|
127
|
+
|
|
128
|
+
init = self.init
|
|
129
|
+
# Map friendly aliases / MATLAB numeric codes
|
|
130
|
+
if isinstance(init, str):
|
|
131
|
+
key = init.lower()
|
|
132
|
+
if key in ("radially", "uniform", "radial"):
|
|
133
|
+
init = 0
|
|
134
|
+
elif key == "random":
|
|
135
|
+
init = 1
|
|
136
|
+
else:
|
|
137
|
+
raise ValueError(
|
|
138
|
+
"init string must be one of "
|
|
139
|
+
"['radially', 'uniform', 'random']; got {!r}".format(init)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if isinstance(init, (int, np.integer)):
|
|
143
|
+
if init == 0:
|
|
144
|
+
# Radially uniform on the half-plane
|
|
145
|
+
max_k = self.K - 1 if self.DC else self.K
|
|
146
|
+
radius = 0.3
|
|
147
|
+
# MATLAB: for k = DC+(1:maxK) with angle (k-1+(m-1)*maxK)
|
|
148
|
+
# 0-based equivalent: angle factor = (k + m*max_k)
|
|
149
|
+
for k in range(int(self.DC), int(self.DC) + max_k):
|
|
150
|
+
for m in range(self.M):
|
|
151
|
+
angle = np.pi * (k + m * max_k) / max_k / self.M
|
|
152
|
+
omega[0, 0, k, m] = radius * np.cos(angle)
|
|
153
|
+
omega[0, 1, k, m] = radius * np.sin(angle)
|
|
154
|
+
elif init == 1:
|
|
155
|
+
for k in range(self.K):
|
|
156
|
+
for m in range(self.M):
|
|
157
|
+
omega[0, 0, k, m] = self.rng.random() - 0.5
|
|
158
|
+
omega[0, 1, k, m] = self.rng.random() / 2.0
|
|
159
|
+
if self.DC:
|
|
160
|
+
omega[0, :, 0, :] = 0.0
|
|
161
|
+
else:
|
|
162
|
+
raise ValueError("numeric init must be 0 (radially) or 1 (random)")
|
|
163
|
+
else:
|
|
164
|
+
arr = np.asarray(init, dtype=float)
|
|
165
|
+
if arr.size != 2 * self.K * self.M:
|
|
166
|
+
raise ValueError(
|
|
167
|
+
"custom init must have size 2*K*M (shape (2, K, M)); "
|
|
168
|
+
"got size {}".format(arr.size)
|
|
169
|
+
)
|
|
170
|
+
arr = arr.reshape(2, self.K, self.M)
|
|
171
|
+
omega[0, :, :, :] = arr
|
|
172
|
+
|
|
173
|
+
return omega
|
|
174
|
+
|
|
175
|
+
def fit_transform(
|
|
176
|
+
self, image: np.ndarray, return_all: bool = False
|
|
177
|
+
) -> Union[
|
|
178
|
+
np.ndarray, Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]
|
|
179
|
+
]:
|
|
180
|
+
"""
|
|
181
|
+
Decompose a 2D image with compact / TV variational mode decomposition.
|
|
182
|
+
|
|
183
|
+
:param image: real 2D array ``(Hy, Hx)``
|
|
184
|
+
:param return_all: if True, also return ``v, omega, A, X``
|
|
185
|
+
:return: modes ``u`` with shape ``(Hy, Hx, K, M)``, or the full tuple
|
|
186
|
+
"""
|
|
187
|
+
signal = np.asarray(image, dtype=float)
|
|
188
|
+
if signal.ndim != 2:
|
|
189
|
+
raise ValueError("image must be a 2-D array")
|
|
190
|
+
hy, hx = signal.shape
|
|
191
|
+
if hy < 4 or hx < 4:
|
|
192
|
+
raise ValueError("image spatial size must be at least 4×4")
|
|
193
|
+
|
|
194
|
+
# Normalised spatial grid (MATLAB meshgrid)
|
|
195
|
+
grid_x, grid_y = np.meshgrid(
|
|
196
|
+
np.arange(1, hx + 1) / hx, np.arange(1, hy + 1) / hy
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Spectral domain discretisation — fy uses Hy (bugfix vs old Hx)
|
|
200
|
+
fx = 1.0 / hx
|
|
201
|
+
fy = 1.0 / hy
|
|
202
|
+
freqs_1 = grid_x - 0.5 - fx
|
|
203
|
+
freqs_2 = grid_y - 0.5 - fy
|
|
204
|
+
|
|
205
|
+
# Storage (Fourier modes are complex)
|
|
206
|
+
u_hat = np.zeros((hy, hx, self.K, self.M), dtype=complex)
|
|
207
|
+
u = np.zeros((hy, hx, self.K, self.M), dtype=float)
|
|
208
|
+
u_old = u.copy()
|
|
209
|
+
v = u.copy()
|
|
210
|
+
|
|
211
|
+
# Augmented Lagrangian variables
|
|
212
|
+
lambda_k = np.zeros_like(u) # u/v linking ~ rho_k
|
|
213
|
+
lambda_d = np.zeros((hy, hx), dtype=float) # data fidelity ~ rho
|
|
214
|
+
|
|
215
|
+
# Spatial supports & artifact map
|
|
216
|
+
A = np.ones((hy, hx, self.K), dtype=float)
|
|
217
|
+
A_old = A.copy()
|
|
218
|
+
artifact = np.zeros((hy, hx), dtype=bool)
|
|
219
|
+
|
|
220
|
+
omega = self._init_omega()
|
|
221
|
+
|
|
222
|
+
u_diff = np.inf
|
|
223
|
+
a_diff = np.inf
|
|
224
|
+
omega_diff = np.inf
|
|
225
|
+
sum_avk = 0.0
|
|
226
|
+
|
|
227
|
+
# Phase-schedule lower bound (handles Inf in A_phase like MATLAB)
|
|
228
|
+
finite_phases = self.A_phase[np.isfinite(self.A_phase)]
|
|
229
|
+
phase_bound = float(np.max(finite_phases)) if finite_phases.size else 0.0
|
|
230
|
+
|
|
231
|
+
# MATLAB starts n = 1; we use 0-based n and compare with (n + 1)
|
|
232
|
+
n = 0
|
|
233
|
+
while n < self.max_iter and (
|
|
234
|
+
(u_diff > self.u_tol or a_diff > self.A_tol or omega_diff > self.omega_tol)
|
|
235
|
+
or (n + 1) <= phase_bound
|
|
236
|
+
):
|
|
237
|
+
# ---- modes / submodes ------------------------------------------------
|
|
238
|
+
for k in range(self.K):
|
|
239
|
+
for m in range(self.M):
|
|
240
|
+
hilbert_mask = (
|
|
241
|
+
np.sign(
|
|
242
|
+
freqs_1 * omega[n, 0, k, m] + freqs_2 * omega[n, 1, k, m]
|
|
243
|
+
)
|
|
244
|
+
+ 1.0
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Update accumulator of A_j v_j for j ≠ (k,m)
|
|
248
|
+
if m == 0:
|
|
249
|
+
if k == 0:
|
|
250
|
+
sum_avk = (
|
|
251
|
+
sum_avk
|
|
252
|
+
+ A[:, :, -1] * v[:, :, -1, -1]
|
|
253
|
+
- A[:, :, 0] * v[:, :, 0, 0]
|
|
254
|
+
)
|
|
255
|
+
else:
|
|
256
|
+
sum_avk = (
|
|
257
|
+
sum_avk
|
|
258
|
+
+ A[:, :, k - 1] * v[:, :, k - 1, -1]
|
|
259
|
+
- A[:, :, k] * v[:, :, k, 0]
|
|
260
|
+
)
|
|
261
|
+
else:
|
|
262
|
+
sum_avk = (
|
|
263
|
+
sum_avk
|
|
264
|
+
+ A[:, :, k] * v[:, :, k, m - 1]
|
|
265
|
+
- A[:, :, k] * v[:, :, k, m]
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
one_minus_x = 1.0 - artifact.astype(float)
|
|
269
|
+
|
|
270
|
+
# Update v (spatial-domain averaging)
|
|
271
|
+
v[:, :, k, m] = (
|
|
272
|
+
self.rho_k * u[:, :, k, m]
|
|
273
|
+
+ lambda_k[:, :, k, m]
|
|
274
|
+
+ self.rho
|
|
275
|
+
* A[:, :, k]
|
|
276
|
+
* (signal - sum_avk + lambda_d / self.rho)
|
|
277
|
+
* one_minus_x
|
|
278
|
+
) / (self.rho_k + self.rho * one_minus_x * A[:, :, k] ** 2)
|
|
279
|
+
|
|
280
|
+
# Update u_hat (analytic spectrum via Wiener filter)
|
|
281
|
+
u_hat[:, :, k, m] = (
|
|
282
|
+
fftshift(
|
|
283
|
+
fft2d(self.rho_k * v[:, :, k, m] - lambda_k[:, :, k, m])
|
|
284
|
+
)
|
|
285
|
+
* hilbert_mask
|
|
286
|
+
) / (
|
|
287
|
+
self.rho_k
|
|
288
|
+
+ 2.0
|
|
289
|
+
* self.alpha
|
|
290
|
+
* (
|
|
291
|
+
(freqs_1 - omega[n, 0, k, m]) ** 2
|
|
292
|
+
+ (freqs_2 - omega[n, 1, k, m]) ** 2
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# Centre frequencies (keep first mode at 0 if DC)
|
|
297
|
+
if (not self.DC) or k > 0:
|
|
298
|
+
power = np.abs(u_hat[:, :, k, m]) ** 2
|
|
299
|
+
denom = np.sum(power)
|
|
300
|
+
if denom > 1e-30:
|
|
301
|
+
omega[n + 1, 0, k, m] = np.sum(freqs_1 * power) / denom
|
|
302
|
+
omega[n + 1, 1, k, m] = np.sum(freqs_2 * power) / denom
|
|
303
|
+
# Keep omegas on the top half-plane
|
|
304
|
+
if omega[n + 1, 1, k, m] < 0:
|
|
305
|
+
omega[n + 1, :, k, m] = -omega[n + 1, :, k, m]
|
|
306
|
+
|
|
307
|
+
# Recover real mode from analytic spectrum
|
|
308
|
+
u[:, :, k, m] = np.real(ifft2d(ifftshift(u_hat[:, :, k, m])))
|
|
309
|
+
|
|
310
|
+
# Phase II: individual MBO / TV support propagation
|
|
311
|
+
# MATLAB: n >= A_phase(1) && n < A_phase(2) (1-based n)
|
|
312
|
+
if self.A_phase[0] <= (n + 1) < self.A_phase[1]:
|
|
313
|
+
one_minus_x = 1.0 - artifact.astype(float)
|
|
314
|
+
sum_v_k = np.sum(v[:, :, k, :], axis=2)
|
|
315
|
+
A[:, :, k] = A[:, :, k] + self.t * (
|
|
316
|
+
-self.beta
|
|
317
|
+
+ 2.0
|
|
318
|
+
* self.rho
|
|
319
|
+
* sum_v_k
|
|
320
|
+
* (
|
|
321
|
+
signal
|
|
322
|
+
- np.sum(A * np.sum(v, axis=3), axis=2)
|
|
323
|
+
+ A[:, :, k] * sum_v_k
|
|
324
|
+
+ lambda_d / self.rho
|
|
325
|
+
)
|
|
326
|
+
* one_minus_x
|
|
327
|
+
)
|
|
328
|
+
A[:, :, k] = A[:, :, k] / (
|
|
329
|
+
1.0 + self.t * 2.0 * self.rho * one_minus_x * sum_v_k**2
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
A[A > 1] = 1.0
|
|
333
|
+
A[A < 0] = 0.0
|
|
334
|
+
|
|
335
|
+
# Heat equation / spectral diffusion
|
|
336
|
+
A[:, :, k] = np.real(
|
|
337
|
+
ifft2d(
|
|
338
|
+
fft2d(A[:, :, k])
|
|
339
|
+
/ (
|
|
340
|
+
1.0
|
|
341
|
+
+ self.t
|
|
342
|
+
* self.gamma
|
|
343
|
+
* ifftshift(freqs_1**2 + freqs_2**2)
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
A[:, :, k] = (A[:, :, k] >= 0.5).astype(float)
|
|
348
|
+
|
|
349
|
+
# Phase III: joint MBO + winner-takes-all segmentation
|
|
350
|
+
if (n + 1) >= self.A_phase[1]:
|
|
351
|
+
sum_av = np.sum(A * np.sum(v, axis=3), axis=2)
|
|
352
|
+
for k in range(self.K):
|
|
353
|
+
sum_v_k = np.sum(v[:, :, k, :], axis=2)
|
|
354
|
+
A[:, :, k] = A[:, :, k] + self.t * (
|
|
355
|
+
-self.beta
|
|
356
|
+
+ 2.0
|
|
357
|
+
* self.rho
|
|
358
|
+
* sum_v_k
|
|
359
|
+
* (signal - sum_av + A[:, :, k] * sum_v_k + lambda_d / self.rho)
|
|
360
|
+
)
|
|
361
|
+
A[:, :, k] = A[:, :, k] / (
|
|
362
|
+
1.0 + self.t * 2.0 * self.rho * sum_v_k**2
|
|
363
|
+
)
|
|
364
|
+
A[:, :, k] = np.real(
|
|
365
|
+
ifft2d(
|
|
366
|
+
fft2d(A[:, :, k])
|
|
367
|
+
/ (
|
|
368
|
+
1.0
|
|
369
|
+
+ self.t
|
|
370
|
+
* self.gamma
|
|
371
|
+
* ifftshift(freqs_1**2 + freqs_2**2)
|
|
372
|
+
)
|
|
373
|
+
)
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# Winner-takes-all (column-major / Fortran order like MATLAB)
|
|
377
|
+
a_flat = A.reshape(hy * hx, self.K, order="F")
|
|
378
|
+
winners = np.argmax(a_flat, axis=1)
|
|
379
|
+
a_new = np.zeros_like(a_flat)
|
|
380
|
+
a_new[np.arange(hy * hx), winners] = 1.0
|
|
381
|
+
A = a_new.reshape(hy, hx, self.K, order="F")
|
|
382
|
+
|
|
383
|
+
# Artifact thresholding
|
|
384
|
+
residual = signal - np.sum(A * np.sum(v, axis=3), axis=2)
|
|
385
|
+
artifact = residual**2 >= self.delta
|
|
386
|
+
|
|
387
|
+
# Dual ascent — data fidelity
|
|
388
|
+
lambda_d = lambda_d + self.tau * residual
|
|
389
|
+
# Dual ascent — u/v splitting (must use tau_k, not tau)
|
|
390
|
+
lambda_k = lambda_k + self.tau_k * (u - v)
|
|
391
|
+
|
|
392
|
+
n += 1
|
|
393
|
+
|
|
394
|
+
# Stopping criteria (match MATLAB formulas)
|
|
395
|
+
u_norm = norm(u.ravel())
|
|
396
|
+
if u_norm > 1e-30:
|
|
397
|
+
u_diff = (norm((u - u_old).ravel()) ** 2) / (u_norm**2) / (hx * hy)
|
|
398
|
+
else:
|
|
399
|
+
u_diff = 0.0
|
|
400
|
+
a_diff = norm(A.ravel() - A_old.ravel(), ord=1) / (hx * hy)
|
|
401
|
+
omega_diff = float(norm(omega[n, :, :, :] - omega[n - 1, :, :, :]) ** 2)
|
|
402
|
+
|
|
403
|
+
u_old = u.copy()
|
|
404
|
+
A_old = A.copy()
|
|
405
|
+
|
|
406
|
+
omega_final = omega[n, :, :, :]
|
|
407
|
+
|
|
408
|
+
if return_all:
|
|
409
|
+
return u, v, omega_final, A, artifact.astype(float)
|
|
410
|
+
return u
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Created on 2025/08/04
|
|
4
|
+
@author: Whenxuan Wang
|
|
5
|
+
@email: wwhenxuan@gmail.com
|
|
6
|
+
"""
|
|
7
|
+
import unittest
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from pysdkit import CVMD2D
|
|
12
|
+
from pysdkit.data import test_grayscale, get_meshgrid_2D
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _small_texture(size: int = 64) -> np.ndarray:
|
|
16
|
+
"""Synthetic multi-orientation texture similar to the paper demos."""
|
|
17
|
+
x, y = get_meshgrid_2D(low=0.0, high=2.0 * np.pi, sampling_rate=size)
|
|
18
|
+
img = 0.8 * np.sin(6.0 * x) + 0.6 * np.sin(5.0 * y) + 0.5 * np.sin(4.0 * (x + y))
|
|
19
|
+
return img - img.mean()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CVMD2DTest(unittest.TestCase):
|
|
23
|
+
"""Unit tests for Compact / 2D-TV Variational Mode Decomposition."""
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def setUpClass(cls) -> None:
|
|
27
|
+
gray = test_grayscale().astype(float)
|
|
28
|
+
cls.image = gray[::4, ::4] - gray[::4, ::4].mean()
|
|
29
|
+
cls.synth = _small_texture(48)
|
|
30
|
+
|
|
31
|
+
def _make(self, **kwargs) -> CVMD2D:
|
|
32
|
+
params = dict(
|
|
33
|
+
K=3,
|
|
34
|
+
alpha=1000,
|
|
35
|
+
beta=0.5,
|
|
36
|
+
gamma=500,
|
|
37
|
+
delta=np.inf,
|
|
38
|
+
rho=10,
|
|
39
|
+
rho_k=10,
|
|
40
|
+
tau=0.0,
|
|
41
|
+
tau_k=2.5,
|
|
42
|
+
t=1.5,
|
|
43
|
+
DC=True,
|
|
44
|
+
init="radially",
|
|
45
|
+
max_iter=35,
|
|
46
|
+
M=1,
|
|
47
|
+
A_phase=np.array([15.0, np.inf]),
|
|
48
|
+
)
|
|
49
|
+
params.update(kwargs)
|
|
50
|
+
return CVMD2D(**params)
|
|
51
|
+
|
|
52
|
+
def test_fit_transform_shape(self) -> None:
|
|
53
|
+
K = 3
|
|
54
|
+
decomp = self._make(K=K)
|
|
55
|
+
u = decomp.fit_transform(self.image)
|
|
56
|
+
hy, hx = self.image.shape
|
|
57
|
+
self.assertEqual(u.shape, (hy, hx, K, 1))
|
|
58
|
+
self.assertTrue(np.all(np.isfinite(u)))
|
|
59
|
+
|
|
60
|
+
def test_default_call(self) -> None:
|
|
61
|
+
decomp = self._make()
|
|
62
|
+
a = decomp(self.image)
|
|
63
|
+
b = decomp.fit_transform(self.image)
|
|
64
|
+
self.assertEqual(a.shape, b.shape)
|
|
65
|
+
self.assertTrue(np.allclose(a, b))
|
|
66
|
+
|
|
67
|
+
def test_return_all(self) -> None:
|
|
68
|
+
decomp = self._make(K=3)
|
|
69
|
+
u, v, omega, A, X = decomp.fit_transform(self.image, return_all=True)
|
|
70
|
+
hy, hx = self.image.shape
|
|
71
|
+
self.assertEqual(u.shape, (hy, hx, 3, 1))
|
|
72
|
+
self.assertEqual(v.shape, u.shape)
|
|
73
|
+
self.assertEqual(omega.shape[1], 3) # (2, K, M)
|
|
74
|
+
self.assertEqual(A.shape, (hy, hx, 3))
|
|
75
|
+
self.assertEqual(X.shape, (hy, hx))
|
|
76
|
+
self.assertTrue(np.all((A >= 0) & (A <= 1)))
|
|
77
|
+
|
|
78
|
+
def test_uniform_init_alias(self) -> None:
|
|
79
|
+
"""'uniform' must map to MATLAB init=0 (radially)."""
|
|
80
|
+
decomp = self._make(init="uniform", max_iter=10, A_phase=np.array([5.0, 8.0]))
|
|
81
|
+
u = decomp.fit_transform(self.synth)
|
|
82
|
+
self.assertTrue(np.all(np.isfinite(u)))
|
|
83
|
+
|
|
84
|
+
def test_random_init(self) -> None:
|
|
85
|
+
decomp = self._make(
|
|
86
|
+
init="random", DC=False, max_iter=12, A_phase=np.array([6.0, 10.0])
|
|
87
|
+
)
|
|
88
|
+
u = decomp.fit_transform(self.synth)
|
|
89
|
+
self.assertEqual(u.shape[-2], 3)
|
|
90
|
+
|
|
91
|
+
def test_dc_mode_stays_at_origin(self) -> None:
|
|
92
|
+
decomp = self._make(K=3, DC=True, max_iter=20, A_phase=np.array([10.0, np.inf]))
|
|
93
|
+
_, _, omega, _, _ = decomp.fit_transform(self.synth, return_all=True)
|
|
94
|
+
self.assertTrue(np.allclose(omega[:, 0, :], 0.0, atol=1e-12))
|
|
95
|
+
|
|
96
|
+
def test_phase_iii_winner_takes_all(self) -> None:
|
|
97
|
+
"""After phase III, supports should form a partition (sum_k A_k = 1)."""
|
|
98
|
+
decomp = self._make(
|
|
99
|
+
K=3,
|
|
100
|
+
max_iter=40,
|
|
101
|
+
A_phase=np.array([10.0, 20.0]),
|
|
102
|
+
tau=0.0,
|
|
103
|
+
tau_k=0.0,
|
|
104
|
+
)
|
|
105
|
+
_, _, _, A, _ = decomp.fit_transform(self.synth, return_all=True)
|
|
106
|
+
self.assertTrue(np.allclose(A.sum(axis=2), 1.0))
|
|
107
|
+
|
|
108
|
+
def test_modes_carry_energy(self) -> None:
|
|
109
|
+
decomp = self._make(K=3, max_iter=40, A_phase=np.array([20.0, np.inf]))
|
|
110
|
+
u = decomp.fit_transform(self.synth)
|
|
111
|
+
energies = [float(np.sum(u[:, :, k, 0] ** 2)) for k in range(3)]
|
|
112
|
+
self.assertTrue(all(e > 0 for e in energies))
|
|
113
|
+
# reconstruction from modes should explain a non-trivial fraction of energy
|
|
114
|
+
recon = u.sum(axis=(2, 3))
|
|
115
|
+
rel = np.linalg.norm(recon - self.synth) / (np.linalg.norm(self.synth) + 1e-30)
|
|
116
|
+
self.assertLess(rel, 1.5)
|
|
117
|
+
|
|
118
|
+
def test_invalid_init(self) -> None:
|
|
119
|
+
with self.assertRaises(ValueError):
|
|
120
|
+
self._make(init="not-a-method").fit_transform(self.synth)
|
|
121
|
+
|
|
122
|
+
def test_non_2d_input(self) -> None:
|
|
123
|
+
with self.assertRaises(ValueError):
|
|
124
|
+
self._make().fit_transform(np.ones(16))
|
|
125
|
+
|
|
126
|
+
def test_str(self) -> None:
|
|
127
|
+
self.assertIn("CVMD2D", str(CVMD2D()))
|
|
128
|
+
|
|
129
|
+
def test_grayscale_demo_params(self) -> None:
|
|
130
|
+
"""MATLAB case-1 style parameters should run on the library grayscale demo."""
|
|
131
|
+
img = test_grayscale().astype(float)
|
|
132
|
+
img = img[::4, ::4] - img[::4, ::4].mean()
|
|
133
|
+
decomp = CVMD2D(
|
|
134
|
+
K=5,
|
|
135
|
+
alpha=1000,
|
|
136
|
+
beta=0.5,
|
|
137
|
+
gamma=500,
|
|
138
|
+
delta=np.inf,
|
|
139
|
+
rho=10,
|
|
140
|
+
rho_k=10,
|
|
141
|
+
tau=2.5,
|
|
142
|
+
tau_k=2.5,
|
|
143
|
+
t=1.5,
|
|
144
|
+
DC=True,
|
|
145
|
+
init="radially",
|
|
146
|
+
max_iter=45,
|
|
147
|
+
M=1,
|
|
148
|
+
A_phase=np.array([25.0, np.inf]),
|
|
149
|
+
)
|
|
150
|
+
u, v, omega, A, X = decomp.fit_transform(img, return_all=True)
|
|
151
|
+
self.assertEqual(u.shape[2], 5)
|
|
152
|
+
self.assertTrue(np.all(np.isfinite(u)))
|
|
153
|
+
self.assertTrue(np.all(np.isfinite(v)))
|
|
154
|
+
self.assertEqual(X.shape, img.shape)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
unittest.main()
|