Skip to content

Commit c1245ec

Browse files
committed
add crop during visu
1 parent 96a38b6 commit c1245ec

5 files changed

Lines changed: 149 additions & 44 deletions

File tree

src/cardiotensor/orientation/orientation_computation_pipeline.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ def compute_orientation(
4040
conf_file_path (str): Path to the configuration file.
4141
start_index (int, optional): Start index for processing. Default is 0.
4242
end_index (int | None): End index for processing. Default is None.
43-
use_gpu (bool, optional): Whether to use GPU for calculations. Default is False.
4443
4544
Returns:
4645
None

src/cardiotensor/scripts/generate_streamlines.py

Lines changed: 53 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ def script():
8383
print(f"⚠️ No eigenvector directory at {eigen_dir}")
8484
sys.exit(1)
8585

86+
87+
#---------------------------------
88+
# BINNING
89+
90+
8691
if bin_factor > 1:
8792
downsample_vector_volume(eigen_dir, bin_factor, OUTPUT_DIR)
8893
vec_load_dir = OUTPUT_DIR / f"bin{bin_factor}" / "eigen_vec"
@@ -93,64 +98,86 @@ def script():
9398
start_binned = start_idx
9499
end_binned = end_idx
95100

101+
FA_dir = OUTPUT_DIR / "FA"
102+
if bin_factor > 1:
103+
downsample_volume(
104+
input_path=FA_dir,
105+
bin_factor=bin_factor,
106+
output_dir=OUTPUT_DIR,
107+
subfolder="FA",
108+
out_ext="tif",
109+
min_value=0,
110+
max_value=255,
111+
)
112+
fa_load_dir = OUTPUT_DIR / f"bin{bin_factor}" / "FA"
113+
else:
114+
fa_load_dir = FA_dir
115+
116+
HA_dir = OUTPUT_DIR / "HA"
117+
if bin_factor > 1:
118+
downsample_volume(
119+
input_path=HA_dir,
120+
bin_factor=bin_factor,
121+
output_dir=OUTPUT_DIR,
122+
subfolder="HA",
123+
out_ext="tif",
124+
min_value=0,
125+
max_value=255,
126+
)
127+
ha_load_dir = OUTPUT_DIR / f"bin{bin_factor}" / "HA"
128+
else:
129+
ha_load_dir = HA_dir
130+
131+
#---------------------------------
132+
133+
134+
96135
print("Loading vector field...")
97136
vec_reader = DataReader(vec_load_dir)
98-
vector_slices = vec_reader.load_volume(
137+
vector_field = vec_reader.load_volume(
99138
start_index=start_binned, end_index=end_binned
100139
)
101140

102-
if vector_slices.ndim == 4 and vector_slices.shape[0] == 3:
103-
vector_field = vector_slices
104-
elif vector_slices.ndim == 4 and vector_slices.shape[-1] == 3:
105-
vector_field = np.moveaxis(vector_slices, -1, 0)
141+
if vector_field.ndim == 4 and vector_field.shape[0] == 3:
142+
vector_field = vector_field
143+
elif vector_field.ndim == 4 and vector_field.shape[-1] == 3:
144+
vector_field = np.moveaxis(vector_field, -1, 0)
106145
else:
107-
print(f"⚠️ Unexpected vector field shape: {vector_slices.shape}")
146+
print(f"⚠️ Unexpected vector field shape: {vector_field.shape}")
108147
sys.exit(1)
109148

110149
print("Ensuring Z-components are positive...")
111150
neg_mask = vector_field[2] < 0
112151
vector_field[:, neg_mask] *= -1
152+
del neg_mask
113153

114154
if MASK_PATH:
115155
print("Applying mask from config...")
116156
mask_reader = DataReader(MASK_PATH)
117157

118158
# Load the corresponding mask volume, resampled to match vector field shape
119-
mask_volume = mask_reader.load_volume(
159+
mask = mask_reader.load_volume(
120160
start_index=start_binned,
121161
end_index=end_binned,
122162
unbinned_shape=vec_reader.shape[1:], # (Z, Y, X)
123163
)
124164

125-
mask = (mask_volume > 0).astype(np.uint8)
126-
165+
mask = (mask > 0).astype(np.uint8)
127166
vector_field[:, mask == 0] = np.nan
128167

129168
# Seed mask from FA
130169
print("Generating seed mask using FA > 0.4...")
131-
FA_dir = OUTPUT_DIR / "FA"
132-
if bin_factor > 1:
133-
downsample_volume(
134-
input_path=FA_dir,
135-
bin_factor=bin_factor,
136-
output_dir=OUTPUT_DIR,
137-
subfolder="FA",
138-
out_ext="tif",
139-
min_value=0,
140-
max_value=255,
141-
)
142-
fa_load_dir = OUTPUT_DIR / f"bin{bin_factor}" / "FA"
143-
else:
144-
fa_load_dir = FA_dir
145170

146171
if not fa_load_dir.exists():
147172
print(f"⚠️ No FA directory found at {fa_load_dir}")
148173
sys.exit(1)
149174

150175
print("Loading FA volume...")
151176
fa_reader = DataReader(fa_load_dir)
152-
fa_volume = fa_reader.load_volume(start_index=start_binned, end_index=end_binned)
153-
seed_mask = (fa_volume > 0.4).astype(np.uint8)
177+
# FA vlume is stored as 8 bits (0-255), so we need to scale it
178+
fa_volume = fa_reader.load_volume(start_index=start_binned, end_index=end_binned)
179+
180+
seed_mask = (fa_volume > (0.25 * 255))
154181

155182
print(f"Selecting {num_seeds} random seed points from mask...")
156183
valid_indices = np.argwhere(seed_mask > 0)
@@ -161,6 +188,7 @@ def script():
161188
chosen_indices = valid_indices[
162189
np.random.choice(valid_indices.shape[0], num_seeds, replace=False)
163190
]
191+
del valid_indices
164192

165193
# Trace streamlines
166194
streamlines = generate_streamlines_from_vector_field(
@@ -175,25 +203,10 @@ def script():
175203
)
176204

177205
# Load HA for sampling
178-
HA_dir = OUTPUT_DIR / "HA"
179206
if not HA_dir.exists():
180207
print(f"⚠️ No HA directory found at {HA_dir}")
181208
sys.exit(1)
182209

183-
if bin_factor > 1:
184-
downsample_volume(
185-
input_path=HA_dir,
186-
bin_factor=bin_factor,
187-
output_dir=OUTPUT_DIR,
188-
subfolder="HA",
189-
out_ext="tif",
190-
min_value=0,
191-
max_value=255,
192-
)
193-
ha_load_dir = OUTPUT_DIR / f"bin{bin_factor}" / "HA"
194-
else:
195-
ha_load_dir = HA_dir
196-
197210
ha_reader = DataReader(ha_load_dir)
198211
HA_volume = ha_reader.load_volume(start_index=start_binned, end_index=end_binned)
199212

src/cardiotensor/scripts/visualize_streamlines.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@
3434
from cardiotensor.utils.utils import read_conf_file
3535

3636

37+
def compute_streamline_bounds(streamlines):
38+
"""
39+
Compute min and max bounds in X, Y, Z for a list of 3D streamlines.
40+
41+
Returns:
42+
((x_min, x_max), (y_min, y_max), (z_min, z_max))
43+
"""
44+
all_points = np.concatenate(streamlines, axis=0)
45+
x_min, y_min, z_min = all_points.min(axis=0)
46+
x_max, y_max, z_max = all_points.max(axis=0)
47+
return (x_min, x_max), (y_min, y_max), (z_min, z_max)
48+
49+
50+
3751
def script():
3852
parser = argparse.ArgumentParser(
3953
description="Visualize streamlines from .npz file."
@@ -90,6 +104,19 @@ def script():
90104
default=None,
91105
help="Maximum number of streamlines to render. If not set, no limit is applied.",
92106
)
107+
108+
parser.add_argument(
109+
"--crop-z", nargs=2, type=float, metavar=("ZMIN", "ZMAX"),
110+
help="Crop streamlines to this Z range (after conversion to (x,y,z))."
111+
)
112+
parser.add_argument(
113+
"--crop-y", nargs=2, type=float, metavar=("YMIN", "YMAX"),
114+
help="Crop streamlines to this Y range."
115+
)
116+
parser.add_argument(
117+
"--crop-x", nargs=2, type=float, metavar=("XMIN", "XMAX"),
118+
help="Crop streamlines to this X range."
119+
)
93120

94121
parser.add_argument(
95122
"--no-interactive",
@@ -117,6 +144,7 @@ def script():
117144
help="Window or screenshot height in pixels. Default: 800.",
118145
)
119146

147+
120148
args = parser.parse_args()
121149
conf_path = Path(args.conf_file)
122150

@@ -166,7 +194,23 @@ def script():
166194
# Convert from [0, 255] to [-90, 90]
167195
ha_values = ha_values.astype(np.float32)
168196
color_values = (ha_values / 255.0) * 180.0 - 90.0
169-
197+
198+
# Build crop bounds with open-ended defaults
199+
crop_z = tuple(args.crop_z) if args.crop_z else (-float("inf"), float("inf"))
200+
crop_y = tuple(args.crop_y) if args.crop_y else (-float("inf"), float("inf"))
201+
crop_x = tuple(args.crop_x) if args.crop_x else (-float("inf"), float("inf"))
202+
203+
if any([args.crop_z, args.crop_y, args.crop_x]):
204+
crop_bounds = (crop_x, crop_y, crop_z)
205+
else:
206+
crop_bounds = None
207+
208+
print(f"Original bounds for streamlines:")
209+
x_bounds, y_bounds, z_bounds = compute_streamline_bounds(streamlines_xyz)
210+
print(f"X bounds: {int(x_bounds[0])} - {int(x_bounds[1])}")
211+
print(f"Y bounds: {int(y_bounds[0])} - {int(y_bounds[1])}")
212+
print(f"Z bounds: {int(z_bounds[0])} - {int(z_bounds[1])}")
213+
170214
show_streamlines(
171215
streamlines_xyz=streamlines_xyz,
172216
color_values=color_values,
@@ -179,6 +223,7 @@ def script():
179223
max_streamlines=args.max_streamlines,
180224
filter_min_len=args.min_length,
181225
subsample_factor=args.subsample,
226+
crop_bounds=crop_bounds,
182227
)
183228

184229

src/cardiotensor/tractography/visualize_streamlines.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def show_streamlines(
3939
max_streamlines: int | None = None,
4040
filter_min_len: int | None = None,
4141
subsample_factor: int = 1,
42+
crop_bounds: tuple[tuple[float, float], tuple[float, float], tuple[float, float]] | None = None,
4243
):
4344
print(f"Initial number of streamlines: {len(streamlines_xyz)}")
4445
if filter_min_len:
@@ -50,6 +51,50 @@ def show_streamlines(
5051
if max_streamlines:
5152
print(f"Limiting to max {max_streamlines} streamlines")
5253

54+
55+
56+
57+
58+
59+
# Crop streamlines and color values (point-wise) if bounds are provided
60+
print(f"Cropping streamlines within bounds: {crop_bounds}" if crop_bounds else "No cropping applied.")
61+
if crop_bounds is not None:
62+
z_min, z_max = crop_bounds[2]
63+
y_min, y_max = crop_bounds[1]
64+
x_min, x_max = crop_bounds[0]
65+
66+
new_streamlines = []
67+
new_color_values = []
68+
color_idx = 0
69+
70+
for sl in streamlines_xyz:
71+
n_pts = len(sl)
72+
cl = color_values[color_idx : color_idx + n_pts]
73+
74+
sl = np.asarray(sl)
75+
cl = np.asarray(cl)
76+
77+
within = (
78+
(sl[:, 0] >= x_min) & (sl[:, 0] <= x_max) &
79+
(sl[:, 1] >= y_min) & (sl[:, 1] <= y_max) &
80+
(sl[:, 2] >= z_min) & (sl[:, 2] <= z_max)
81+
)
82+
83+
if np.any(within): # Keep only remaining points
84+
new_sl = sl[within]
85+
new_cl = cl[within]
86+
if len(new_sl) > 0:
87+
new_streamlines.append(new_sl)
88+
new_color_values.append(new_cl)
89+
90+
color_idx += n_pts
91+
92+
streamlines_xyz = new_streamlines
93+
color_values = np.concatenate(new_color_values) if new_color_values else np.array([])
94+
else:
95+
print("No cropping applied.")
96+
97+
5398
# Downsample and filter
5499
downsampled_streamlines = []
55100
downsampled_colors = []
@@ -88,6 +133,11 @@ def show_streamlines(
88133

89134
print(f"Final number of streamlines to render: {len(streamlines_xyz)}")
90135

136+
137+
138+
if not color_values:
139+
raise ValueError("❌ No streamlines left after filtering and cropping. Adjust parameters like --crop or --min-length.")
140+
91141
flat_colors = np.concatenate(color_values)
92142
print(f"Coloring mode: min={flat_colors.min():.2f}, max={flat_colors.max():.2f}")
93143
print(f"Rendering mode: {mode}")

src/cardiotensor/utils/downsampling.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,6 @@ def process_image_block(
246246
downsampling in XY, converting to 8-bit, and writing to disk.
247247
248248
Args:
249-
z_start (int): Start index of the Z-block.
250-
z_end (int): End index of the Z-block (exclusive).
251249
file_list (list): List of file paths (entire volume stack).
252250
bin_factor (int): Binning factor for XY downsampling.
253251
out_file (Path): Output file path for the downsampled image.

0 commit comments

Comments
 (0)