cardiotensor 1.0.3__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.
File without changes
File without changes
@@ -0,0 +1,242 @@
1
+ import math
2
+
3
+ import matplotlib.pyplot as plt
4
+ import numpy as np
5
+ import pandas as pd
6
+ from skimage.measure import profile_line
7
+
8
+
9
+ def _calculate_angle_line(
10
+ start: tuple[float, float], end: tuple[float, float]
11
+ ) -> float:
12
+ """
13
+ Calculate the angle of a line defined by start and end points.
14
+
15
+ Parameters:
16
+ start (Tuple[float, float]): The starting point of the line (x1, y1).
17
+ end (Tuple[float, float]): The ending point of the line (x2, y2).
18
+
19
+ Returns:
20
+ float: The angle of the line in degrees.
21
+ """
22
+ x1, y1 = start
23
+ x2, y2 = end
24
+
25
+ delta_x = x2 - x1
26
+ delta_y = y2 - y1
27
+
28
+ # Calculate the angle in radians
29
+ angle_rad = math.atan2(delta_y, delta_x)
30
+
31
+ # Convert the angle to degrees
32
+ angle_deg = math.degrees(angle_rad)
33
+
34
+ return angle_deg
35
+
36
+
37
+ def find_end_points(
38
+ start_point: tuple[float, float],
39
+ end_point: tuple[float, float],
40
+ angle_range: float,
41
+ N_line: int,
42
+ ) -> np.ndarray:
43
+ """
44
+ Find the end points for lines at different angles within a range.
45
+
46
+ Parameters:
47
+ start_point (Tuple[int, int]): The starting point of the main line.
48
+ end_point (Tuple[int, int]): The ending point of the main line.
49
+ angle_range (float): The range of angles to consider in degrees.
50
+ N_line (int): The number of lines to generate within the range.
51
+
52
+ Returns:
53
+ np.ndarray: Array of end points for the generated lines.
54
+ """
55
+ theta = _calculate_angle_line(start_point, end_point)
56
+
57
+ if N_line > 1:
58
+ theta_list = np.linspace(
59
+ theta - angle_range / 2, theta + angle_range / 2, N_line
60
+ )
61
+ else:
62
+ theta_list = [theta]
63
+
64
+ vector = np.array(end_point) - np.array(start_point)
65
+ norm = np.linalg.norm(vector)
66
+
67
+ # Calculate end points
68
+ end_points = []
69
+ for angle in theta_list:
70
+ theta = np.deg2rad(angle)
71
+ end_x = int(start_point[0] + norm * np.cos(theta))
72
+ end_y = int(start_point[1] + norm * np.sin(theta))
73
+ end_points.append((end_x, end_y))
74
+
75
+ return np.array(end_points)
76
+
77
+
78
+ def calculate_intensities(
79
+ img_helix: np.ndarray,
80
+ start_point: tuple[int, int],
81
+ end_point: tuple[int, int],
82
+ angle_range: float = 5,
83
+ N_line: int = 10,
84
+ max_value: float | None = None,
85
+ min_value: float | None = None,
86
+ ) -> list[np.ndarray]:
87
+ """
88
+ Calculate intensity profiles along multiple lines.
89
+
90
+ Parameters:
91
+ img_helix (np.ndarray): The image array.
92
+ start_point (Tuple[int, int]): The starting point of the line.
93
+ end_point (Tuple[int, int]): The ending point of the line.
94
+ angle_range (float, optional): The range of angles to consider in degrees. Default is 5.
95
+ N_line (int, optional): The number of lines to generate. Default is 10.
96
+ max_value (Optional[float], optional): Maximum value for intensity normalization. Default is None.
97
+ min_value (Optional[float], optional): Minimum value for intensity normalization. Default is None.
98
+
99
+ Returns:
100
+ List[np.ndarray]: List of intensity profiles for each line.
101
+ """
102
+ end_points = find_end_points(start_point, end_point, angle_range, N_line)
103
+
104
+ img_helix[np.isnan(img_helix)] = 0
105
+
106
+ intensity_profiles = []
107
+ for i, end in enumerate(end_points):
108
+ print(f"Measure {i + 1}/{len(end_points)}")
109
+ intensity_profile = profile_line(img_helix, start_point, end, order=0)
110
+
111
+ if min_value is not None and max_value is not None:
112
+ intensity_profile = (
113
+ intensity_profile * (max_value - min_value) / 255 + min_value
114
+ )
115
+
116
+ intensity_profiles.append(intensity_profile)
117
+
118
+ return intensity_profiles
119
+
120
+
121
+ def plot_intensity(
122
+ intensity_profiles: list[np.ndarray],
123
+ label_y: str = "",
124
+ x_max_lim: float | None = None,
125
+ x_min_lim: float | None = None,
126
+ y_max_lim: float | None = None,
127
+ y_min_lim: float | None = None,
128
+ ) -> None:
129
+ """
130
+ Plot intensity profiles with mean and percentile shading.
131
+
132
+ Parameters:
133
+ intensity_profiles (List[np.ndarray]): List of intensity profiles.
134
+ label_y (str, optional): Label for the y-axis. Default is an empty string.
135
+ x_max_lim (Optional[float], optional): Maximum x-axis limit. Default is None.
136
+ x_min_lim (Optional[float], optional): Minimum x-axis limit. Default is None.
137
+ y_max_lim (Optional[float], optional): Maximum y-axis limit. Default is None.
138
+ y_min_lim (Optional[float], optional): Minimum y-axis limit. Default is None.
139
+ """
140
+ plt.figure(figsize=(10, 6))
141
+
142
+ # Get the minimum length of the profiles
143
+ min_length = min(
144
+ intensity_profile.shape[0] for intensity_profile in intensity_profiles
145
+ )
146
+
147
+ # Trim the arrays to the minimum length
148
+ trimmed_arrays = [
149
+ intensity_profile[:min_length] for intensity_profile in intensity_profiles
150
+ ]
151
+
152
+ # Convert list of trimmed arrays to a 2D NumPy array
153
+ intensity_profiles = np.stack(trimmed_arrays)
154
+
155
+ # Calculate mean and median arrays
156
+ mean_array = np.mean(intensity_profiles, axis=0)
157
+
158
+ # Calculate the 5th and 95th percentiles
159
+ lower_percentile = np.percentile(intensity_profiles, 5, axis=0)
160
+ upper_percentile = np.percentile(intensity_profiles, 95, axis=0)
161
+
162
+ # Create a normalised x-axis ranging from 0 to 1
163
+ x_axis_arr = np.linspace(0, 1, len(mean_array))
164
+
165
+ # Plot the mean
166
+ plt.plot(
167
+ x_axis_arr, mean_array, label="Mean", linewidth=2, color="k"
168
+ ) # Make the line thicker for better visibility
169
+
170
+ # Add shaded area for the 5th to 95th percentiles
171
+ plt.fill_between(
172
+ x_axis_arr,
173
+ lower_percentile,
174
+ upper_percentile,
175
+ color="gray",
176
+ alpha=0.5,
177
+ label="5%-95% Percentiles",
178
+ )
179
+
180
+ # Increase axis and label thickness
181
+ ax = plt.gca() # Get current axis
182
+ ax.spines["top"].set_linewidth(2)
183
+ ax.spines["right"].set_linewidth(2)
184
+ ax.spines["left"].set_linewidth(2)
185
+ ax.spines["bottom"].set_linewidth(2)
186
+
187
+ # Increase tick width and font size
188
+ ax.xaxis.set_tick_params(width=2)
189
+ ax.yaxis.set_tick_params(width=2)
190
+ plt.xticks(fontsize=12, weight="bold")
191
+ plt.yticks(fontsize=12, weight="bold")
192
+
193
+ # Set thicker labels with larger font size
194
+ plt.xlabel("Normalised Transmural Depth", fontsize=14, weight="bold")
195
+ plt.ylabel(label_y, fontsize=14, weight="bold")
196
+
197
+ # Set axis limits if provided
198
+ if x_max_lim:
199
+ plt.xlim(
200
+ [x_min_lim, x_max_lim]
201
+ ) # Set the x-axis limits, e.g., plt.xlim([0, 1])
202
+ if y_max_lim:
203
+ plt.ylim(
204
+ [y_min_lim, y_max_lim]
205
+ ) # Set the y-axis limits, e.g., plt.ylim([min_value, max_value])
206
+
207
+ # Show legend and plot
208
+ # plt.legend(fontsize=12)
209
+ plt.show()
210
+
211
+
212
+ def save_intensity(intensity_profiles: list[np.ndarray], save_path: str) -> None:
213
+ """
214
+ Save intensity profiles to a CSV file.
215
+
216
+ Parameters:
217
+ intensity_profiles (List[np.ndarray]): List of intensity profiles.
218
+ save_path (str): Path to save the CSV file.
219
+
220
+ Returns:
221
+ None
222
+ """
223
+ # Convert intensity_profiles into a DataFrame
224
+ df = pd.DataFrame(intensity_profiles)
225
+
226
+ # Rename columns to start from "Value 1"
227
+ df.columns = [f"Value {i + 1}" for i in range(df.shape[1])]
228
+
229
+ # Add a "Profile" row
230
+ df.insert(0, "Profile", [f"Profile {i + 1}" for i in range(df.shape[0])])
231
+
232
+ # Transpose the DataFrame so profiles are in columns
233
+ df = df.transpose()
234
+
235
+ # Set the first row as the header
236
+ df.columns = df.iloc[0]
237
+ df = df[1:]
238
+
239
+ # Save DataFrame to CSV with a semicolon delimiter
240
+ df.to_csv(save_path, sep=";", index=False)
241
+
242
+ print(f"Profile saved to {save_path}")