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.
- cardiotensor/__init__.py +0 -0
- cardiotensor/analysis/__init__.py +0 -0
- cardiotensor/analysis/analysis_functions.py +242 -0
- cardiotensor/analysis/gui_analysis_tool.py +733 -0
- cardiotensor/export/__init__.py +0 -0
- cardiotensor/export/amira_writer.py +374 -0
- cardiotensor/export/vtk_writer.py +350 -0
- cardiotensor/launcher/__init__.py +0 -0
- cardiotensor/launcher/slurm_launcher.py +260 -0
- cardiotensor/orientation/__init__.py +0 -0
- cardiotensor/orientation/orientation_computation_functions.py +614 -0
- cardiotensor/orientation/orientation_computation_pipeline.py +387 -0
- cardiotensor/scripts/amira_writer.py +71 -0
- cardiotensor/scripts/compute_orientation.py +136 -0
- cardiotensor/scripts/gui_analysis_tool.py +57 -0
- cardiotensor/scripts/slurm_launcher.py +44 -0
- cardiotensor/scripts/vtk_writer.py +27 -0
- cardiotensor/utils/DataReader.py +371 -0
- cardiotensor/utils/__init__.py +0 -0
- cardiotensor/utils/downsampling.py +171 -0
- cardiotensor/utils/utils.py +140 -0
- cardiotensor-1.0.3.dist-info/METADATA +168 -0
- cardiotensor-1.0.3.dist-info/RECORD +27 -0
- cardiotensor-1.0.3.dist-info/WHEEL +5 -0
- cardiotensor-1.0.3.dist-info/entry_points.txt +6 -0
- cardiotensor-1.0.3.dist-info/licenses/LICENSE +21 -0
- cardiotensor-1.0.3.dist-info/top_level.txt +1 -0
cardiotensor/__init__.py
ADDED
|
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}")
|