Velocity analysis¶
Radon velocity analysis estimates blood flow speed along a kymograph ROI by finding the dominant streak angle in sliding time windows. Each window produces one velocity sample at a center time, giving a velocity-versus-time series. That series is also the starting point for heart rate analysis.
This notebook walks through:
- Load an
AcqImagefrom a sample file. - Select a channel/ROI on a line-scan kymograph.
- Understand the single detection parameter (
window_width). - Run Radon velocity analysis.
- Plot and inspect the result table.
The key parameter: window width¶
window_width is the number of time rows in each Radon window. A wider window smooths noise but blurs fast changes; a narrower window is more responsive but noisier. There is no automatic accept/reject check here (unlike heart rate's dual-estimator agreement) — you judge quality from the trace and summary statistics.
Where this fits in CloudScope¶
This notebook uses the acqstore scripting API directly. In everyday use you would explore window_width interactively in the CloudScope GUI on a representative file.
Once you have settled on a value, AcqImageList lets you apply the same fixed window_width across many files so velocity results stay comparable. The velocity time-series produced here is also the required input to heart rate analysis on the same channel/ROI.
import numpy as np
import pandas as pd
import plotly.graph_objects as go
import plotly.io as pio
pio.renderers.default = "notebook"
from acqstore.acq_image.analysis import RadonVelocityAnalysis
from acqstore.acq_image import AcqImage
from acqstore.acq_image import AcqImageList
from acqstore.sample_data import ensure_sample
Load a sample file¶
We use the reproducible velocity-sample-data sample (Olympus .oir line-scan kymographs with saved velocity results). Replace ensure_sample(...) with a local folder path when adapting this notebook to your own .oir, .czi, .tif, or .ome.zarr data.
sample_folder = ensure_sample('velocity-sample-data')
acq_list = AcqImageList(str(sample_folder))
acq = acq_list.get_files()[0]
# Be explicit: list the available channels and ROIs, then pick from them.
channel_indices = acq.images.channel_indices
roi_ids = acq.rois.get_roi_ids()
CHANNEL = channel_indices[0]
ROI_ID = roi_ids[0]
print("file:", acq.name)
print("channels:", channel_indices, "roi_ids:", roi_ids)
print("channel:", CHANNEL, "roi:", ROI_ID)
Failed to load sidecar JSON for /Users/cudmore/Library/Application Support/cloudscope/sample-data/velocity-sample-data-v1/velocity-sample-data/14d Saline/20251014/20251014_A98_0002.oir: 'heart_rate' requires 'radon_velocity' for channel=0, roi_id=1
file: 20251014_A98_0002.oir channels: [0] roi_ids: [1] channel: 0 roi: 1
Detection parameters¶
Radon velocity has a single tunable detection parameter. The schema lists its default and allowed choices.
RadonVelocityAnalysis.get_detection_schema_dataframe()
| display_name | type | default | choices | unit | editable | visible | methods | category | description | |
|---|---|---|---|---|---|---|---|---|---|---|
| name | ||||||||||
| window_width | Window Width | int | 64 | (16, 64, 128) | None | True | True | None | None | Number of time samples per Radon analysis window. |
What window_width does¶
- Window width (
window_width, default64) — number of time samples per Radon window. Allowed values:16,64,128. - Trade-off — narrow windows (16) follow fast changes but are noisier; wide windows (128) smooth the trace but blur brief events.
- Output — one velocity sample per window center, stored in a CSV table with
time_sandvelocitycolumns, plus a small summary dict (velocity_mean,velocity_median,velocity_cv, ...).
Run velocity analysis¶
We run analysis fresh here with create_and_run(), which creates the analysis and runs it in a single call. Radon velocity can use multiprocessing, which is unsafe inside Jupyter on macOS, so we pass execution_options={"use_multiprocessing": False} to run serially. Expect roughly 30 seconds on this sample ROI.
# Choose detection parameters. Defaults come from the schema above; here we set
# the Radon window width explicitly (try 16 or 128 to see the effect).
detection_params = {"window_width": 64}
# create_and_run() creates the analysis and runs it in one call. We disable
# multiprocessing because it is unsafe inside Jupyter on macOS (see note above).
velocity = acq.analysis_set.create_and_run(
RadonVelocityAnalysis,
channel=CHANNEL,
roi_id=ROI_ID,
detection_params=detection_params,
replace_existing=True,
execution_options={"use_multiprocessing": False},
)
print("summary:")
for key, value in velocity.result.summary.items():
print(f" {key}: {value}")
summary: analysis_date: 260707 analysis_time: 17:39:42.671 analysis_version: 1 num_windows: 1872 velocity_mean: 22.358151153988505 velocity_median: 25.84040613337845 velocity_cv: 1.6838492359691521
Plot velocity versus time¶
get_plot_data() returns display-ready time_s / velocity arrays and axis labels without needing to know the table column names.
plot_data = velocity.get_plot_data()
fig = go.Figure(
go.Scatter(x=plot_data.x, y=plot_data.y, mode="lines", name=plot_data.series_name)
)
fig.update_layout(
title=f"Radon velocity - {acq.name} ch={CHANNEL} roi={ROI_ID}",
xaxis_title=plot_data.x_label,
yaxis_title=plot_data.y_label,
margin={"l": 60, "r": 20, "t": 50, "b": 50},
)
fig.show()
Inspect the result table¶
The analysis stores a per-window table. When you call acq.save() it is written to a CSV file next to the recording. Useful columns include time_s, velocity, and theta_deg (the Radon angle for each window).
velocity.result.table.head(10)
| time_s | time_index | theta_deg | velocity | |
|---|---|---|---|---|
| 0 | 0.036287 | 32.0 | 84.00 | 26.971985 |
| 1 | 0.054430 | 48.0 | 84.00 | 26.971985 |
| 2 | 0.072574 | 64.0 | 84.25 | 25.840406 |
| 3 | 0.090717 | 80.0 | 84.25 | 25.840406 |
| 4 | 0.108861 | 96.0 | 84.50 | 24.709821 |
| 5 | 0.127004 | 112.0 | 84.25 | 25.840406 |
| 6 | 0.145148 | 128.0 | 84.25 | 25.840406 |
| 7 | 0.163291 | 144.0 | 84.25 | 25.840406 |
| 8 | 0.181435 | 160.0 | 84.25 | 25.840406 |
| 9 | 0.199578 | 176.0 | 84.25 | 25.840406 |
Save and load results¶
Calling acq.save() writes the analysis to two files next to the recording: a JSON file with the detection parameters and summary statistics, and a CSV file with the per-window table. A later session can load these files back without re-running the analysis, as shown below.
# Save the analysis to files next to the recording.
acq.save()
# Open the recording again and load the saved analysis back from those files.
# We look it up by type, channel, and ROI, so we do not need the object from above.
reloaded = AcqImage(acq.path)
loaded_analysis = reloaded.analysis_set.get_analysis(
RadonVelocityAnalysis, channel=CHANNEL, roi_id=ROI_ID
)
print("loaded summary:", loaded_analysis.result.summary)
loaded summary: {'analysis_date': '260707', 'analysis_time': '17:39:42.671', 'analysis_version': 1, 'num_windows': 1872, 'velocity_cv': 1.6838492359691521, 'velocity_mean': 22.358151153988505, 'velocity_median': 25.84040613337845}
Takeaways¶
- Radon velocity produces a velocity-versus-time series from a kymograph ROI.
- The only detection parameter is
window_width(window size in time samples). - In notebooks, pass
execution_options={"use_multiprocessing": False}tocreate_and_runso the analysis runs serially and stays stable. - This velocity series is the required input for heart rate analysis on the same channel/ROI.
- Fix
window_widthacross anAcqImageListwhen batch-processing many files.