Skip to content

Commit a2b7dc1

Browse files
committed
Merge branch 'main' into napari-curvealign
2 parents b1b72ff + 80ee91c commit a2b7dc1

File tree

3 files changed

+225
-14
lines changed

3 files changed

+225
-14
lines changed

src/pycurvelets/get_tif_boundary.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,8 @@ def get_tif_boundary(coordinates, img, obj, dist_thresh, min_dist):
213213
"nearest_boundary_angle",
214214
"extension_point_distance",
215215
"extension_point_angle",
216-
"boundary_point_col",
217216
"boundary_point_row",
217+
"boundary_point_col",
218218
]
219219

220220
result_df = pd.DataFrame(result_mat, columns=result_mat_names)

src/pycurvelets/process_image.py

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -875,12 +875,12 @@ def save_fiber_features(
875875
# Build fiber features DataFrame with descriptive column names
876876
fib_feat_data = {
877877
"fiber_key": fiber_key if fiber_key else list(range(len(fiber_structure))),
878-
"end_point_row": (
878+
"center_row": (
879879
fiber_structure["center_row"]
880880
if "center_row" in fiber_structure.columns
881881
else fiber_structure["center_1"]
882882
),
883-
"end_point_col": (
883+
"center_col": (
884884
fiber_structure["center_col"]
885885
if "center_col" in fiber_structure.columns
886886
else fiber_structure["center_2"]
@@ -922,8 +922,8 @@ def save_fiber_features(
922922
"nearest_boundary_angle": "nearest_relative_boundary_angle",
923923
"extension_point_distance": "extension_point_distance",
924924
"extension_point_angle": "extension_point_angle",
925-
"boundary_point_row": "boundary_point_row",
926925
"boundary_point_col": "boundary_point_col",
926+
"boundary_point_row": "boundary_point_row",
927927
}
928928
for src_col, dest_col in boundary_col_mapping.items():
929929
if src_col in measured_boundary.columns:
@@ -938,8 +938,8 @@ def save_fiber_features(
938938
"nearest_relative_boundary_angle",
939939
"extension_point_distance",
940940
"extension_point_angle",
941-
"boundary_point_row",
942941
"boundary_point_col",
942+
"boundary_point_row",
943943
]
944944
for col_name in boundary_cols:
945945
fib_feat_df[col_name] = np.nan
@@ -1226,15 +1226,10 @@ def generate_overlay(
12261226
# Plot lines connecting each fiber center to its nearest boundary point
12271227
for center, bndry_pt in zip(in_curvs, in_bndry):
12281228
if not np.isnan(bndry_pt[0]) and not np.isnan(bndry_pt[1]):
1229-
# center : [row, col]
1230-
# bndry_pt : [col, row] (NOTE: boundary points are stored as x, y)
1231-
# matplotlib plot expects (x=col, y=row)
1232-
# MATLAB equivalent:
1233-
# plot([center(1,2) bndry(1)], [center(1,1) bndry(2)], 'b')
1234-
1229+
# center : [col, row], bndry_pt: [col, row]
12351230
ax.plot(
1236-
[center[1], bndry_pt[0]], # x: center col → boundary col
1237-
[center[0], bndry_pt[1]], # y: center row → boundary row
1231+
[center[1], bndry_pt[1]], # x: center col → boundary col
1232+
[center[0], bndry_pt[0]], # y: center row → boundary row
12381233
"b-",
12391234
linewidth=0.5,
12401235
)

tests/test_process_image.py

Lines changed: 217 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
# Optional: attempt import; skip module if curvelet backend is missing
3434
try:
35-
from pycurvelets.process_image import process_image
35+
from pycurvelets.process_image import process_image, generate_overlay
3636
except ModuleNotFoundError:
3737
pytest.skip(
3838
"curvelops not available; skipping process_image tests", allow_module_level=True
@@ -140,6 +140,27 @@ def load_boundary_data_from_json(boundary_params_dict, img_name):
140140
return BoundaryParameters(**boundary_params_dict)
141141

142142

143+
def is_association_test_case(test_case):
144+
"""
145+
Return True only for test cases where generate_overlay will draw association lines.
146+
This requires make_associations=True, make_overlay=True, and tif_boundary=3.
147+
"""
148+
output_params = test_case.get("output_params", {})
149+
boundary_params = test_case.get("boundary_params")
150+
return (
151+
output_params.get("make_associations", False)
152+
and output_params.get("make_overlay", False)
153+
and boundary_params is not None
154+
and boundary_params.get("tif_boundary") == 3
155+
)
156+
157+
158+
all_cases = load_test_cases()
159+
association_cases = [
160+
(name, tc) for name, tc in all_cases if is_association_test_case(tc)
161+
]
162+
163+
143164
# --------------------------
144165
# Tests
145166
# --------------------------
@@ -249,6 +270,201 @@ def test_process_image_returns_fiber_features(test_name, test_case, tmp_path):
249270
print(f"⚠ Reference CSV not found: {reference_csv_name}")
250271

251272

273+
@pytest.mark.parametrize(
274+
"test_name,test_case",
275+
association_cases,
276+
ids=[name for name, _ in association_cases],
277+
)
278+
def test_generate_overlay(test_name, test_case, tmp_path, monkeypatch):
279+
"""
280+
Verify that generate_overlay draws each association line with the correct
281+
row/col coordinate ordering.
282+
283+
For every fiber that has a non-NaN boundary point in fib_feat_df we expect
284+
ax.plot to be called with:
285+
286+
x = [fiber_center_col, boundary_point_col] (i.e. center[1], bndry_pt[0])
287+
y = [fiber_center_row, boundary_point_row] (i.e. center[0], bndry_pt[1])
288+
289+
A bug would flip one or both pairs, e.g. using boundary_point_row as an
290+
x-coordinate or boundary_point_col as a y-coordinate.
291+
"""
292+
# --- Run process_image to get fib_feat_df ---
293+
img = load_test_image(test_case["image_params"]["img"])
294+
295+
image_params = ImageInputParameters(
296+
img=img,
297+
img_name=test_case["image_params"]["img_name"],
298+
slice_num=1,
299+
num_sections=1,
300+
)
301+
fiber_params = FiberAnalysisParameters(
302+
**{**test_case["fiber_params"], "fire_directory": str(tmp_path)}
303+
)
304+
output_params = OutputControlParameters(
305+
**{**test_case["output_params"], "output_directory": str(tmp_path)}
306+
)
307+
boundary_params = load_boundary_data_from_json(
308+
test_case["boundary_params"],
309+
test_case["image_params"]["img_name"],
310+
)
311+
advanced_options = AdvancedAnalysisOptions(**test_case["advanced_options"])
312+
313+
results = process_image(
314+
image_params=image_params,
315+
fiber_params=fiber_params,
316+
output_params=output_params,
317+
boundary_params=boundary_params,
318+
advanced_options=advanced_options,
319+
)
320+
321+
assert (
322+
results is not None and "fib_feat_df" in results
323+
), "process_image must return fib_feat_df for this test to run"
324+
fib_feat_df = results["fib_feat_df"]
325+
326+
# Fibers that have a valid boundary association
327+
valid_fibers = fib_feat_df.dropna(
328+
subset=["boundary_point_row", "boundary_point_col"]
329+
)
330+
assert len(valid_fibers) > 0, (
331+
f"Test case '{test_name}' has no fibers with valid boundary points; "
332+
"cannot verify association-line coordinates."
333+
)
334+
335+
# --- Intercept ax.plot calls made inside generate_overlay ---
336+
# Each captured entry: {"x": [...], "y": [...]}
337+
captured_blue_lines = []
338+
339+
original_plot = plt.Axes.plot
340+
341+
def spy_plot(self, *args, **kwargs):
342+
# Association lines are drawn as "b-" with linewidth=0.5
343+
is_blue = (len(args) >= 3 and args[2] == "b-") or kwargs.get("color") in (
344+
"b",
345+
"blue",
346+
)
347+
if is_blue and len(args) >= 2:
348+
captured_blue_lines.append({"x": list(args[0]), "y": list(args[1])})
349+
return original_plot(self, *args, **kwargs)
350+
351+
monkeypatch.setattr(plt.Axes, "plot", spy_plot)
352+
353+
# --- Reconstruct measured_boundary in the shape generate_overlay expects ---
354+
#
355+
# analyze_global_boundary returns a slice of res_df with these columns:
356+
# nearest_boundary_distance, nearest_region_distance, nearest_boundary_angle,
357+
# extension_point_distance, extension_point_angle,
358+
# boundary_point_col, boundary_point_row
359+
#
360+
# save_fiber_features renames most of them but keeps boundary_point_row and
361+
# boundary_point_col verbatim, so we can recover them from fib_feat_df directly.
362+
# The remaining columns are not accessed by generate_overlay, so we fill them
363+
# with NaN to satisfy the DataFrame shape without misrepresenting data.
364+
measured_boundary = pd.DataFrame(
365+
{
366+
"nearest_boundary_distance": fib_feat_df[
367+
"nearest_distance_to_boundary"
368+
].values,
369+
"nearest_region_distance": fib_feat_df["inside_epicenter_region"].values,
370+
"nearest_boundary_angle": fib_feat_df[
371+
"nearest_relative_boundary_angle"
372+
].values,
373+
"extension_point_distance": fib_feat_df["extension_point_distance"].values,
374+
"extension_point_angle": fib_feat_df["extension_point_angle"].values,
375+
# These two are what generate_overlay actually reads:
376+
"boundary_point_col": fib_feat_df["boundary_point_col"].values,
377+
"boundary_point_row": fib_feat_df["boundary_point_row"].values,
378+
},
379+
index=fib_feat_df.index,
380+
)
381+
382+
# Reconstructing fiber_structure to be in a format that generate_overlay
383+
# expects: "fiber_absolute_angle" is changed to "angle" (see save_fiber_features)
384+
fiber_structure = fib_feat_df.rename(
385+
columns={
386+
"fiber_absolute_angle": "angle",
387+
}
388+
)
389+
390+
# --- Call generate_overlay directly with the reconstructed data ---
391+
coordinates = boundary_params.coordinates if boundary_params else None
392+
n_fibers = len(fib_feat_df)
393+
in_curvs_flag = np.ones(n_fibers, dtype=bool) # include every fiber
394+
out_curvs_flag = np.zeros(n_fibers, dtype=bool)
395+
nearest_angles = fiber_structure["angle"].values
396+
397+
generate_overlay(
398+
img=img,
399+
fiber_structure=fiber_structure,
400+
coordinates=coordinates,
401+
in_curvs_flag=in_curvs_flag,
402+
out_curvs_flag=out_curvs_flag,
403+
nearest_angles=nearest_angles,
404+
measured_boundary=measured_boundary,
405+
output_directory=str(tmp_path),
406+
img_name=test_case["image_params"]["img_name"],
407+
fiber_mode=0,
408+
tif_boundary=3,
409+
boundary_measurement=True,
410+
make_associations=True,
411+
num_sections=1,
412+
)
413+
414+
assert len(captured_blue_lines) > 0, (
415+
"generate_overlay drew no blue association lines even though "
416+
f"make_associations=True and {len(valid_fibers)} fibers have boundary points."
417+
)
418+
419+
expected_lines = []
420+
for idx in valid_fibers.index:
421+
center_row = fiber_structure.at[idx, "center_row"]
422+
center_col = fiber_structure.at[idx, "center_col"]
423+
bp_row = measured_boundary.at[idx, "boundary_point_row"]
424+
bp_col = measured_boundary.at[idx, "boundary_point_col"]
425+
expected_lines.append(
426+
{
427+
"x": [center_col, bp_col],
428+
"y": [center_row, bp_row],
429+
"fiber_idx": idx,
430+
}
431+
)
432+
433+
# Match each expected line to a captured line (within floating-point tolerance)
434+
unmatched = []
435+
for exp in expected_lines:
436+
found = any(
437+
np.allclose(cap["x"], exp["x"], atol=0.05)
438+
and np.allclose(cap["y"], exp["y"], atol=15)
439+
for cap in captured_blue_lines
440+
)
441+
if not found:
442+
unmatched.append(exp)
443+
444+
# Provide a clear failure message showing the first few mismatches
445+
if unmatched:
446+
examples = unmatched[:5]
447+
msg_lines = [
448+
f"{len(unmatched)} / {len(expected_lines)} association lines have wrong coordinates.",
449+
"",
450+
"Each line should be plotted as:",
451+
" x = [center_col, boundary_point_col]",
452+
" y = [center_row, boundary_point_row]",
453+
"",
454+
"First mismatches (expected → not found among captured lines):",
455+
]
456+
for e in examples:
457+
msg_lines.append(f" fiber {e['fiber_idx']}: " f"x={e['x']}, y={e['y']}")
458+
msg_lines += [
459+
"",
460+
"Sample of captured blue lines:",
461+
]
462+
for cap in captured_blue_lines[:5]:
463+
msg_lines.append(f" x={cap['x']}, y={cap['y']}")
464+
465+
pytest.fail("\n".join(msg_lines))
466+
467+
252468
if __name__ == "__main__":
253469
# Run tests with pytest
254470
pytest.main([__file__, "-v"])

0 commit comments

Comments
 (0)