Skip to content

Conversation

@gdevenyi
Copy link
Member

@gdevenyi gdevenyi commented Dec 17, 2025

Summary by CodeRabbit

  • New Features
    • Added a command-line tool to compute framewise displacement metrics from an image mask and a sequence of rigid-body transforms.
    • Produces per-timepoint mean and maximum displacement values (t=0 set to zero) and writes results to CSV.
    • Includes input validation and a visual progress indicator during processing.
    • CLI supports specifying input files, output location, and verbosity.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 17, 2025

Walkthrough

Adds a new CLI tool that computes per-timepoint framewise displacement (mean and max) by extracting ROI points from a binary mask, reading a CSV of rigid-body transforms, applying transforms sequentially to points, and writing results to an output CSV.

Changes

Cohort / File(s) Summary
Framewise Displacement Calculation Tool
framewise_displacement.py
New file adding calculate_framewise_displacement(mask_file, csv_file, output_csv=None, verbose=False) and main() CLI: loads a binary mask via SimpleITK and extracts ROI voxel physical coordinates; reads transform matrices from a CSV; initializes reference points from the first transform; computes per-timepoint mean and max FD by applying successive transforms and measuring Euclidean distances; shows progress with tqdm; validates input files and writes an output CSV with headers Timepoint, MeanFD, MaxFD.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI
    participant FS as File System
    participant Mask as Mask Processor
    participant Trans as Transform Reader
    participant FD as FD Calculator
    participant Out as Output Writer

    User->>CLI: run mask_file, csv_file, [output_csv], --verbose
    CLI->>FS: check files exist
    FS-->>CLI: exist / error
    CLI->>Mask: load mask, extract ROI voxel physical points
    Mask->>FS: read mask file
    Mask-->>CLI: ROI points
    CLI->>Trans: read transforms CSV -> transforms[]
    Trans->>FS: read CSV
    Trans-->>CLI: transforms[]
    CLI->>FD: initialize prev_points using transforms[0] (TransformPoint)
    loop for each transform t in transforms[1:]
        CLI->>FD: apply transform t -> cur_points
        FD->>FD: compute Euclidean distances(cur_points, prev_points)
        FD-->>CLI: mean_fd, max_fd for t
        CLI->>FD: set prev_points = cur_points
    end
    CLI->>Out: write CSV (Timepoint, MeanFD, MaxFD)
    Out->>FS: write output file
    FS-->>Out: success
    Out-->>CLI: done
    CLI-->>User: print/return results
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Inspect framewise_displacement.py for correct voxel-to-physical coordinate conversion and use of SimpleITK TransformPoint
  • Verify CSV parsing and expected transform matrix format and ordering
  • Check handling of edge cases: empty ROI mask, fewer than two transforms, file I/O and permission errors

Poem

🐇 I hopped through masks and matrix lines,

Points leapt forward and then aligned,
Mean and max I softly trace,
Rows of motion in tidy space,
CSV crumbs from a joyful chase.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Point wise implementation of frame-wise displacement' clearly relates to the main change: a new framewise_displacement.py utility that computes framewise displacement using point-wise transformations on ROI voxels.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch framewise_displacement

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
framewise_displacement.py (2)

11-16: Remove unused helper function.

This function is never called. The same logic is duplicated inline at lines 67-73. Consider removing this function to reduce code duplication and improve maintainability.

Apply this diff to remove the dead code:

-def calculate_mean_max_fd(points, transform, prev_points):
-    curr_points = np.array([transform.TransformPoint(p) for p in points])
-    distances = np.linalg.norm(curr_points - prev_points, axis=1)
-    mean_fd = np.mean(distances)
-    max_fd = np.max(distances)
-    return mean_fd, max_fd
-

31-31: Consider adding strict=True to zip() for Python 3.10+ compatibility.

As per static analysis hint, adding strict=True ensures all iterables have the same length. While not critical here (the arrays from np.where are guaranteed to match), it's a defensive best practice.

-    for z, y, x in zip(z_idxs, y_idxs, x_idxs):
+    for z, y, x in zip(z_idxs, y_idxs, x_idxs, strict=True):
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7c4c301 and 730483e.

📒 Files selected for processing (1)
  • framewise_displacement.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
framewise_displacement.py (1)
apply_transforms.py (1)
  • read_transforms_from_csv (10-56)
🪛 Ruff (0.14.8)
framewise_displacement.py

31-31: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

🔇 Additional comments (1)
framewise_displacement.py (1)

1-9: LGTM!

The imports are well-organized and appropriate for the tool's functionality.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
framewise_displacement.py (2)

16-29: Validate that the mask is not empty.

If the mask contains no non-zero voxels, the points list will be empty. This will cause np.mean and np.max on lines 67-68 to return nan or issue warnings. Add validation after extracting ROI points to fail fast with a clear error message.

Apply this diff to add validation:

     points = []
     for z, y, x in zip(z_idxs, y_idxs, x_idxs):
         pt = mask.TransformIndexToPhysicalPoint((int(x), int(y), int(z)))
         points.append(pt)
     
+    if len(points) == 0:
+        print("Error: Mask contains no voxels.")
+        sys.exit(1)
+    
     if verbose:
         print(f"ROI contains {len(points)} voxels.")

Based on past review comments, this issue was previously flagged but not yet addressed.


3-9: Move csv import to module level.

The csv module is imported locally on line 80 but should be imported at the top of the file with other standard library imports for consistency and clarity.

Apply this diff to move the import:

+import csv
 import argparse
 import SimpleITK as sitk

Then remove the local import on line 80:

     if output_csv:
         with open(output_csv, 'w', newline='') as f:
-            import csv
             writer = csv.writer(f)

Based on past review comments.

🧹 Nitpick comments (1)
framewise_displacement.py (1)

24-24: Consider adding strict=True to zip() (Python 3.10+).

While the arrays from np.where are guaranteed to have the same length, adding strict=True makes the intent explicit and follows Python 3.10+ best practices.

Apply this diff:

-    for z, y, x in zip(z_idxs, y_idxs, x_idxs):
+    for z, y, x in zip(z_idxs, y_idxs, x_idxs, strict=True):

Note: This requires Python 3.10 or later. Only apply if the project's minimum Python version supports it.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c931db2 and 6f773cf.

📒 Files selected for processing (1)
  • framewise_displacement.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
framewise_displacement.py (1)
apply_transforms.py (1)
  • read_transforms_from_csv (10-56)
🪛 Ruff (0.14.8)
framewise_displacement.py

24-24: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

🔇 Additional comments (3)
framewise_displacement.py (3)

44-54: LGTM: Correct initialization of reference points and baseline displacement.

The initialization properly transforms ROI points using the first transform and correctly sets t=0 displacement to zero by definition. The comment about direct vs. inverse transforms is helpful context.


56-76: LGTM: Correct framewise displacement calculation.

The loop correctly:

  • Transforms points at each timepoint using the corresponding transform
  • Computes Euclidean distances between consecutive point sets using np.linalg.norm(axis=1)
  • Tracks mean and max displacement metrics
  • Updates the reference points for the next iteration

The indexing is correct: loop variable i ranges from 0 to len(transforms)-2, producing timepoints 1 to len(transforms)-1.


92-111: LGTM: Well-structured CLI with proper validation.

The CLI entry point correctly:

  • Defines required and optional arguments with clear help text
  • Validates file existence before processing
  • Provides accurate description of the point-tracking approach
  • Passes arguments to the calculation function

The description accurately reflects the implementation (addressed from past review comments).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
framewise_displacement.py (1)

3-9: Move csv import to module level.

The csv module is currently imported inside the function at line 84, which is non-idiomatic. Import it here with the other standard library imports.

Apply this diff:

 import argparse
+import csv
 import SimpleITK as sitk

Then remove the import csv statement from line 84:

     if output_csv:
         with open(output_csv, 'w', newline='') as f:
-            import csv
             writer = csv.writer(f)

Based on past review comments.

🧹 Nitpick comments (1)
framewise_displacement.py (1)

24-24: Consider adding strict=True to zip() for consistency.

While the arrays from np.where are guaranteed to have the same length, adding strict=True makes the intent explicit and follows modern Python best practices.

Apply this diff:

-    for z, y, x in zip(z_idxs, y_idxs, x_idxs):
+    for z, y, x in zip(z_idxs, y_idxs, x_idxs, strict=True):

Note: This requires Python 3.10+. If targeting earlier versions, skip this change.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6f773cf and 4274c7c.

📒 Files selected for processing (1)
  • framewise_displacement.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
framewise_displacement.py (1)
apply_transforms.py (1)
  • read_transforms_from_csv (10-56)
🪛 Ruff (0.14.8)
framewise_displacement.py

24-24: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

🔇 Additional comments (6)
framewise_displacement.py (6)

11-18: LGTM!

Function signature is well-designed with appropriate parameter names and defaults. The verbose logging for mask reading is helpful.


28-30: LGTM!

The empty mask validation properly addresses the concern from past reviews, preventing downstream failures in np.mean and np.max calls.


35-41: LGTM!

The validation correctly ensures at least 2 transforms are present before attempting framewise displacement calculation. The reuse of read_transforms_from_csv promotes code consistency across the codebase.


43-80: LGTM!

The point-tracking framewise displacement implementation is correct:

  • Properly initializes reference points from t=0 (line 51)
  • Correctly sets t=0 displacement to zero (lines 53-58)
  • Accurately computes per-timepoint displacements by comparing consecutive transformed point sets (lines 66-69)
  • The comments about transform direction (lines 49, 65) are helpful and accurate

The use of tqdm for progress visualization is user-friendly.


82-92: LGTM!

The conditional CSV output properly handles the optional output_csv parameter (line 82), and the output format with clear headers is appropriate. This addresses the past concern about handling output_csv=None.


96-112: LGTM!

The CLI implementation is clean and includes proper input validation. The description "using point tracking" accurately reflects the implementation method, addressing the concern from past reviews.

@Gab-D-G
Copy link
Contributor

Gab-D-G commented Jan 19, 2026

I reviewed the script and it all looks sensible, no comments on my end. Based on my preliminary usage, the framewise displacement estimates seem sensible and similar to other methods.

@gdevenyi gdevenyi merged commit 66303f3 into master Jan 19, 2026
1 check passed
@coderabbitai coderabbitai bot mentioned this pull request Jan 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants