hbs 0.1.1__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.
hbs-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.1
2
+ Name: hbs
3
+ Version: 0.1.1
4
+ Summary: Python library for computing Harmonic Beltrami Signature(HBS)
5
+ Home-page: https://github.com/ChanceAroundYou/hbs_python
6
+ Author: Lin Chenran
7
+ Author-email: chenranlin.17@gmail.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+
14
+ # HBS - Harmonic Beltrami Signature
15
+
16
+ This is a Python library for computing Harmonic Beltrami Signature(HBS). It provides a set of tools for boundary conditions, mesh generation, conformal welding and other mathematical problems, particularly suited for numerical computations in complex analysis.
17
+
18
+ ## Reference
19
+ This implementation is based on the paper:
20
+
21
+ **Harmonic Beltrami Signature: A Novel 2D Shape Representation for Object Classification**
22
+ Chenran Lin, Lok Ming Lui
23
+ DOI: [10.1137/22M1470852](https://doi.org/10.1137/22M1470852)
24
+
25
+ ## Installation
26
+
27
+ Install directly from PyPI:
28
+ ```bash
29
+ pip install hbs
30
+ ```
31
+
32
+ Or install from source:
33
+ 1. Clone this repository:
34
+ ```bash
35
+ git clone https://github.com/ChanceAroundYou/hbs_python.git
36
+ ```
37
+ 2. Install dependencies:
38
+ ```bash
39
+ pip install -r requirements.txt
40
+ ```
41
+
42
+ ## Main Modules
43
+
44
+ - `hbs.py`: HBS algorithm for computing HBS and recontructing shape from HBS
45
+ - `boundary.py`: Boundary processing tools
46
+ - `conformal_welding.py`: Conformal welding algorithm implementation
47
+ - `mesh.py`: Mesh generation and processing
48
+ - `utils/`:
49
+ - `geodesic_welding.py`: Geodesic welding
50
+ - `mobius.py`: Möbius transformations
51
+ - `poisson.py`: Poisson integral implementation
52
+ - `tool_functions.py`: Utility functions
53
+ - `zipper.py`: Zipper algorithm implementation
54
+ - `qc/`: Quasiconformal mapping algorithms
55
+ - `bc.py`: Beltrami coefficient computation
56
+ - `lsqc.py`: Least squares quasiconformal mapping algorithm
57
+
58
+ ## Usage Example
59
+
60
+ Compute HBS from image
61
+
62
+ ```python
63
+ from boundary import get_boundary
64
+ from hbs import get_hbs
65
+
66
+ img_path = 'img/example.jpg'
67
+ bound = get_boundary(img_path, bound_point_num)
68
+ hbs, he, cw, disk = get_hbs(bound, circle_point_num, density)
69
+ ```
70
+
71
+ Reconstruct shape from HBS
72
+ ```python
73
+ from hbs import reconstruct_from_hbs
74
+
75
+ ## `disk` must be a DiskMesh corresponding to give `hbs`
76
+ bound, _, _, _ = reconstruct_from_hbs(hbs, disk)
77
+ ```
78
+
79
+ Please also refer to `example.ipynb`
80
+
81
+ ## Dependencies
82
+
83
+ - NumPy
84
+ - SciPy
85
+ - Matplotlib
86
+ - Opencv
87
+
88
+ ## Contributing
89
+
90
+ Contributions are welcome via pull requests.
91
+
92
+ ## Code Author
93
+ - Chenran Lin
hbs-0.1.1/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # HBS - Harmonic Beltrami Signature
2
+
3
+ This is a Python library for computing Harmonic Beltrami Signature(HBS). It provides a set of tools for boundary conditions, mesh generation, conformal welding and other mathematical problems, particularly suited for numerical computations in complex analysis.
4
+
5
+ ## Reference
6
+ This implementation is based on the paper:
7
+
8
+ **Harmonic Beltrami Signature: A Novel 2D Shape Representation for Object Classification**
9
+ Chenran Lin, Lok Ming Lui
10
+ DOI: [10.1137/22M1470852](https://doi.org/10.1137/22M1470852)
11
+
12
+ ## Installation
13
+
14
+ Install directly from PyPI:
15
+ ```bash
16
+ pip install hbs
17
+ ```
18
+
19
+ Or install from source:
20
+ 1. Clone this repository:
21
+ ```bash
22
+ git clone https://github.com/ChanceAroundYou/hbs_python.git
23
+ ```
24
+ 2. Install dependencies:
25
+ ```bash
26
+ pip install -r requirements.txt
27
+ ```
28
+
29
+ ## Main Modules
30
+
31
+ - `hbs.py`: HBS algorithm for computing HBS and recontructing shape from HBS
32
+ - `boundary.py`: Boundary processing tools
33
+ - `conformal_welding.py`: Conformal welding algorithm implementation
34
+ - `mesh.py`: Mesh generation and processing
35
+ - `utils/`:
36
+ - `geodesic_welding.py`: Geodesic welding
37
+ - `mobius.py`: Möbius transformations
38
+ - `poisson.py`: Poisson integral implementation
39
+ - `tool_functions.py`: Utility functions
40
+ - `zipper.py`: Zipper algorithm implementation
41
+ - `qc/`: Quasiconformal mapping algorithms
42
+ - `bc.py`: Beltrami coefficient computation
43
+ - `lsqc.py`: Least squares quasiconformal mapping algorithm
44
+
45
+ ## Usage Example
46
+
47
+ Compute HBS from image
48
+
49
+ ```python
50
+ from boundary import get_boundary
51
+ from hbs import get_hbs
52
+
53
+ img_path = 'img/example.jpg'
54
+ bound = get_boundary(img_path, bound_point_num)
55
+ hbs, he, cw, disk = get_hbs(bound, circle_point_num, density)
56
+ ```
57
+
58
+ Reconstruct shape from HBS
59
+ ```python
60
+ from hbs import reconstruct_from_hbs
61
+
62
+ ## `disk` must be a DiskMesh corresponding to give `hbs`
63
+ bound, _, _, _ = reconstruct_from_hbs(hbs, disk)
64
+ ```
65
+
66
+ Please also refer to `example.ipynb`
67
+
68
+ ## Dependencies
69
+
70
+ - NumPy
71
+ - SciPy
72
+ - Matplotlib
73
+ - Opencv
74
+
75
+ ## Contributing
76
+
77
+ Contributions are welcome via pull requests.
78
+
79
+ ## Code Author
80
+ - Chenran Lin
@@ -0,0 +1,6 @@
1
+ """HBS - Harmonic Beltrami Signature package.
2
+
3
+ A Python library for complex analysis and conformal mapping computations.
4
+ """
5
+
6
+ from .hbs import get_hbs, reconstruct_from_hbs
@@ -0,0 +1,155 @@
1
+ import cv2
2
+ import matplotlib.pyplot as plt
3
+ import numpy as np
4
+ from scipy.interpolate import CubicSpline, PchipInterpolator
5
+
6
+
7
+ def get_boundary(image_path, num_points=250):
8
+ """
9
+ Process the image to extract boundary, apply anti-aliasing, and resample.
10
+
11
+ Parameters:
12
+ image_path: Input image path
13
+ num_points: Number of resampled points
14
+
15
+ Returns:
16
+ resampled_points: List of resampled boundary points
17
+ """
18
+ # Extract boundary points
19
+ boundary_points = extract_boundary_points(image_path)
20
+ # Apply anti-aliasing
21
+ smoothed_points = anti_aliasing(boundary_points)
22
+ # Resample to the specified number of points
23
+ resampled_points = smooth_resample(smoothed_points, num_points)
24
+ return resampled_points
25
+
26
+
27
+ def extract_boundary_points(image_path):
28
+ """
29
+ Extract ordered boundary points from a near black-and-white image.
30
+
31
+ Parameters:
32
+ image_path: Input image path
33
+
34
+ Returns:
35
+ boundary_points: List of ordered boundary points, format [(x1, y1), (x2, y2), ...]
36
+ """
37
+ # Read the image and convert to grayscale
38
+ img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
39
+
40
+ # Determine if the image is black-on-white or white-on-black
41
+ if np.mean(img) > 127:
42
+ # White-on-black image, invert it
43
+ img = cv2.bitwise_not(img)
44
+
45
+ # Binarize the image
46
+ _, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY)
47
+
48
+ # Find contours (using CHAIN_APPROX_NONE to get all boundary points)
49
+ contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
50
+
51
+ # Get the largest contour (assuming there's only one main shape in the image)
52
+ main_contour = max(contours, key=cv2.contourArea)
53
+
54
+ # Return the contour points directly, without sorting or anti-aliasing
55
+ return main_contour.reshape(-1, 2)
56
+
57
+
58
+ def anti_aliasing(points, kernel_size=5):
59
+ """
60
+ Apply anti-aliasing to boundary points.
61
+
62
+ Parameters:
63
+ points: List of original boundary points
64
+
65
+ Returns:
66
+ smoothed_points: List of smoothed boundary points
67
+ """
68
+ # Use Gaussian blur for smoothing
69
+ x = cv2.GaussianBlur(points[:, 0].astype("float32"), (kernel_size, kernel_size), 0)
70
+ y = cv2.GaussianBlur(points[:, 1].astype("float32"), (kernel_size, kernel_size), 0)
71
+ return np.column_stack((x, y))
72
+
73
+
74
+ def plot_boundary(boundary, with_line=True, scale=1):
75
+ num_points = boundary.shape[0]
76
+ x_min, x_max = np.min(boundary[:, 0]), np.max(boundary[:, 0])
77
+ y_min, y_max = np.min(boundary[:, 1]), np.max(boundary[:, 1])
78
+ aspect_ratio = (x_max - x_min) / (y_max - y_min)
79
+
80
+ fig_size = (num_points / 60 * scale * aspect_ratio, num_points / 60 * scale)
81
+ plt.figure(figsize=fig_size)
82
+ if with_line:
83
+ plt.plot(
84
+ boundary[:, 0],
85
+ boundary[:, 1],
86
+ linestyle="-",
87
+ linewidth=1 * scale,
88
+ color="white",
89
+ label="Boundary Line",
90
+ )
91
+ plt.scatter(
92
+ boundary[:, 0],
93
+ boundary[:, 1],
94
+ marker="o",
95
+ s=15 * scale,
96
+ edgecolor="lightgreen",
97
+ facecolor="none",
98
+ label="Boundary Points",
99
+ )
100
+ plt.show()
101
+
102
+
103
+ def smooth_resample(points, num_points=None):
104
+ """
105
+ Smoothly and uniformly resample the boundary points matrix, generating a matrix of m points.
106
+ :param points: nx2 matrix, original boundary point coordinates
107
+ :param num_points: Number of resampled points
108
+ :return: mx2 matrix, resampled boundary points
109
+ """
110
+ if num_points is None:
111
+ num_points = points.shape[0]
112
+
113
+ # Check if the boundary is closed
114
+ is_closed = np.all(np.abs(points[0, :] - points[-1, :]) < 1e-8)
115
+ if not is_closed:
116
+ points = np.vstack([points, points[0, :]]) # Close the boundary if not closed
117
+
118
+ # Calculate cumulative arc length
119
+ diffs = np.diff(points, axis=0)
120
+ dists = np.sqrt(np.sum(diffs**2, axis=1))
121
+ cum_dists = np.concatenate([[0], np.cumsum(dists)])
122
+ total_length = cum_dists[-1]
123
+
124
+ # Spline interpolation settings
125
+ t = cum_dists
126
+ x = points[:, 0]
127
+ y = points[:, 1]
128
+
129
+ if is_closed:
130
+ # Periodic cubic spline interpolation (closed curve)
131
+ ppx = CubicSpline(t, x, bc_type="periodic")
132
+ ppy = CubicSpline(t, y, bc_type="periodic")
133
+ else:
134
+ # Shape-preserving piecewise cubic interpolation (open curve)
135
+ ppx = PchipInterpolator(t, x)
136
+ ppy = PchipInterpolator(t, y)
137
+
138
+ # Generate uniform parameters
139
+ if is_closed:
140
+ t_new = np.linspace(0, total_length, num_points + 2)
141
+ t_new = t_new[:-1] # Avoid duplicate start and end points
142
+ else:
143
+ t_new = np.linspace(0, total_length, num_points + 1)
144
+
145
+ # Calculate new points
146
+ x_new = ppx(t_new)
147
+ y_new = ppy(t_new)
148
+ resampled_points = np.column_stack([x_new, y_new])
149
+ return resampled_points[:-1, :]
150
+
151
+
152
+ if __name__ == "__main__":
153
+ # 示例用法
154
+ boundary_points = extract_boundary_points("test_image.png")
155
+ print(f"提取的边界点数量:{len(boundary_points)}")
@@ -0,0 +1,150 @@
1
+ import matplotlib.pyplot as plt
2
+ import numpy as np
3
+ from scipy.interpolate import interp1d
4
+
5
+ from utils.mobius import mobius, mobius_inv
6
+ from utils.tool_functions import to_complex
7
+ from utils.zipper import zipper, zipper_params
8
+
9
+
10
+ class ConformalWelding:
11
+ def __init__(
12
+ self,
13
+ x: np.ndarray[np.complexfloating],
14
+ y: np.ndarray[np.complexfloating],
15
+ params: np.ndarray[np.complexfloating],
16
+ ):
17
+ """
18
+ Initialize the ConformalWelding class.
19
+
20
+ Parameters:
21
+ x (np.ndarray): Complex ndarray of shape (n,) representing initial x points.
22
+ y (np.ndarray): Complex ndarray of shape (n,) representing initial y points.
23
+ params (np.ndarray): Complex ndarray of shape (n+1,) representing parameters.
24
+ """
25
+ assert (
26
+ x.ndim == y.ndim == params.ndim == 1
27
+ and np.issubdtype(x.dtype, np.complexfloating)
28
+ and np.issubdtype(y.dtype, np.complexfloating)
29
+ and np.issubdtype(params.dtype, np.complexfloating)
30
+ ), "Input must be 1D complex array"
31
+ assert len(x) == len(y) == len(params) - 1, (
32
+ "Length of x, y, params must be n, n, n+1"
33
+ )
34
+
35
+ self.params = params
36
+ self._set_init_x(x)
37
+ self._set_init_y(y)
38
+
39
+ def _set_init_x(self, x: np.ndarray[np.complexfloating]):
40
+ self.init_x = x
41
+ self.init_x_angle = np.angle(x)
42
+ self.x = x
43
+
44
+ def _set_init_y(self, y: np.ndarray[np.complexfloating]):
45
+ self.init_y = y
46
+ self.y = y
47
+
48
+ def uniquify(self):
49
+ x_angle, idx = np.unique(np.angle(self.init_x), return_index=True)
50
+ x = np.exp(x_angle * 1j)
51
+ y = self.init_y[idx]
52
+ y = np.exp(np.angle(y) * 1j)
53
+
54
+ self._set_init_x(x)
55
+ self._set_init_y(y)
56
+
57
+ def linear_interp(self, num: int):
58
+ x_angle_regular = np.arange(0, 2 * np.pi, 2 * np.pi / num)
59
+ self.x = np.exp(x_angle_regular * 1j)
60
+
61
+ inter_func = interp1d(
62
+ np.concatenate(
63
+ [
64
+ self.init_x_angle - 2 * np.pi,
65
+ self.init_x_angle,
66
+ self.init_x_angle + 2 * np.pi,
67
+ ]
68
+ ),
69
+ np.concatenate([self.init_y, self.init_y, self.init_y]),
70
+ kind="linear",
71
+ )
72
+ y = inter_func(x_angle_regular)
73
+ self.y = np.exp(np.angle(y / y[0]) * 1j)
74
+
75
+ def rotate_x(self, r):
76
+ x_angle = np.mod(self.init_x_angle - r, 2 * np.pi)
77
+ x = np.exp(x_angle * 1j)
78
+ self._set_init_x(x)
79
+
80
+ def x_post_norm(self):
81
+ """
82
+ 对外部点进行后归一化
83
+ :param z: 输入点
84
+ :param params: 参数
85
+ :return: 归一化后的点,a 和 theta 参数
86
+ """
87
+ zinf = zipper_params([np.inf], self.params)
88
+ a = -1 / np.conj(zinf)
89
+ theta = 0
90
+ x = mobius_inv(self.x, a, theta)
91
+ x = np.flipud(x)
92
+ self._set_init_x(x)
93
+
94
+ def y_post_norm(self):
95
+ """
96
+ 对内部点进行后归一化
97
+ :param z: 输入点
98
+ :return: 归一化后的点,a 和 theta 参数
99
+ """
100
+ p = np.append(self.y, [0, 1])
101
+ while True:
102
+ center = np.mean(p[:-2])
103
+ if np.abs(center) <= np.finfo(float).eps:
104
+ break
105
+ p = mobius(p, center)
106
+
107
+ y = p[:-2]
108
+ # y = np.flipud(y)
109
+ self._set_init_y(y)
110
+
111
+ def plot_x(self):
112
+ plt.gca().set_aspect("equal", adjustable="box")
113
+ plt.scatter(self.x.real, self.x.imag)
114
+ plt.show()
115
+
116
+ def plot_y(self):
117
+ plt.gca().set_aspect("equal", adjustable="box")
118
+ plt.scatter(self.y.real, self.y.imag)
119
+ plt.show()
120
+
121
+ def plot(self, is_interp=True):
122
+ plt.gca().set_aspect("equal", adjustable="box")
123
+ x_angle = np.angle(self.x)
124
+ y_angle = np.angle(self.y)
125
+ x_angle = np.mod(x_angle, 2 * np.pi)
126
+ y_angle = np.mod(y_angle, 2 * np.pi)
127
+ if is_interp:
128
+ plt.plot(x_angle, y_angle, linestyle="-", linewidth=2)
129
+ else:
130
+ plt.scatter(x_angle, y_angle, s=2)
131
+ plt.show()
132
+
133
+
134
+ def get_conformal_welding(bound: np.ndarray[np.floating]) -> ConformalWelding:
135
+ assert (
136
+ bound.ndim == 2
137
+ and bound.shape[1] == 2
138
+ and np.issubdtype(bound.dtype, np.floating)
139
+ ), "bound must be n x 2 real array with float type"
140
+
141
+ bound = to_complex(bound)
142
+ x, _, x_params = zipper(bound)
143
+ # x = np.flipud(x)
144
+ y, _, _ = zipper(np.flipud(bound))
145
+
146
+ cw = ConformalWelding(x, y, x_params)
147
+ cw.x_post_norm()
148
+ cw.y_post_norm()
149
+ cw.uniquify()
150
+ return cw
hbs-0.1.1/hbs/hbs.py ADDED
@@ -0,0 +1,103 @@
1
+ import numpy as np
2
+
3
+ from hbs.conformal_welding import ConformalWelding, get_conformal_welding
4
+ from utils.geodesic_welding import geodesic_welding
5
+ from hbs.mesh import DiskMesh, get_unit_disk
6
+ from qc import get_beltrami_coefficient, lsqc_solver
7
+ from utils.poisson import integral as poisson_integral
8
+ from utils.tool_functions import to_complex, to_real
9
+
10
+
11
+ def get_hbs(
12
+ bound: np.ndarray[np.floating],
13
+ circle_point_num: int = 500,
14
+ density: float = 0.01,
15
+ disk: DiskMesh = None,
16
+ ) -> tuple[
17
+ np.ndarray[np.complexfloating],
18
+ np.ndarray[np.floating],
19
+ ConformalWelding,
20
+ DiskMesh,
21
+ ]:
22
+ """
23
+ Get Beltrami coefficients from boundary points
24
+ :param `bound`: `n x 2` array, boundary points
25
+ :param `circle_point_num`: number of points on the circle
26
+ :param `density`: density of the mesh
27
+ :param `disk`: DiskMesh object
28
+ :return:
29
+ `hbs`: `n` x 1 complex array, Beltrami coefficients defined on triangles
30
+ `he`: `n` x 1 float array, harmonice extension of `hbs`
31
+ `cw`: ConformalWelding object
32
+ `disk`: DiskMesh object
33
+ """
34
+ if disk is None:
35
+ disk = get_unit_disk(density, circle_point_num)
36
+
37
+ cw = get_conformal_welding(bound)
38
+
39
+ r = 0
40
+ while True:
41
+ cw.rotate_x(r / 2)
42
+ cw.linear_interp(circle_point_num)
43
+ he = poisson_integral(disk.in_vert, cw.x, cw.y)
44
+ hbs = get_beltrami_coefficient(he, disk)
45
+
46
+ excluded_idx = np.isnan(hbs) + np.linalg.norm(disk.face_center, axis=1) == 0
47
+ r = np.angle(np.sum(hbs[~excluded_idx]))
48
+
49
+ if abs(r) <= 5e-3:
50
+ he_angle = np.angle(np.sum(hbs * to_complex(disk.face_center)))
51
+ if he_angle < 0 or he_angle == np.pi:
52
+ r += 2 * np.pi
53
+ else:
54
+ break
55
+
56
+ return hbs, he, cw, disk
57
+
58
+
59
+ def reconstruct_from_hbs(
60
+ hbs: np.ndarray[np.complexfloating], disk: DiskMesh
61
+ ):
62
+ """
63
+ Reconstruct original shape from HBS
64
+ :param `hbs`: complex array with length `disk.face_num`, Beltrami coefficients defined on triangles
65
+ :param `disk`: DiskMesh object
66
+ :return:
67
+ `bound_points`: `disk.circle_num` x 2 array, boundary points
68
+ `in_points`: `disk.in_vert_num` x 2 array, inner points
69
+ `out_points`: `disk.out_vert_num` x 2 array, outer points
70
+ `mapping`: `disk.vert_num` x 2 array, vertex coordinates of the solved mapping
71
+ """
72
+ assert isinstance(disk, DiskMesh), "`disk` must be DiskMesh object"
73
+
74
+ assert isinstance(hbs, np.ndarray) and np.issubdtype(
75
+ hbs.dtype, np.complexfloating
76
+ ), "`hbs` must be complex array"
77
+ assert hbs.ndim == 1 and hbs.shape[0] == disk.face_num, (
78
+ "the length of `hbs` is `n` and `n` is the number of faces"
79
+ )
80
+
81
+ target = np.array([[1, 0], [0, 0]], dtype=np.float64)
82
+
83
+ one_pos = np.all(disk.vert == target[0], axis=1)
84
+ zero_pos = np.all(disk.vert == target[1], axis=1)
85
+ landmark = np.where(one_pos | zero_pos)[0]
86
+
87
+ mapping = lsqc_solver(hbs, landmark, target, disk)
88
+ bound_points = mapping[: disk.circle_num]
89
+ in_points = mapping[: disk.in_vert_num + disk.circle_num]
90
+ out_points = mapping[disk.in_vert_num + disk.circle_num :]
91
+
92
+ in_points, out_points = geodesic_welding(
93
+ to_complex(in_points),
94
+ to_complex(out_points),
95
+ to_complex(bound_points),
96
+ to_complex(disk.circle),
97
+ )
98
+ in_points = to_real(in_points)
99
+ out_points = to_real(out_points)
100
+
101
+ bound_points = in_points[: disk.circle_num]
102
+ in_points = in_points[disk.circle_num :]
103
+ return bound_points, in_points, out_points, mapping