hbs 0.1.1__py3-none-any.whl

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/__init__.py ADDED
@@ -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
hbs/boundary.py ADDED
@@ -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/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