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 demo-small sample. 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("demo-small")
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)
file: 20251030_A106_0002.oir channels: [0] roi_ids: [1, 2, 3, 4] 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 | description | |
|---|---|---|---|---|---|---|---|---|---|
| name | |||||||||
| window_width | Window Width | int | 64 | (16, 64, 128) | None | True | True | 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: num_windows: 1872 velocity_mean: 4.023862192353049 velocity_median: 3.9538996252060663 velocity_cv: 0.15191629119135608
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.017121 | 32.0 | 78.00 | 4.534540 |
| 1 | 0.025681 | 48.0 | 78.75 | 4.243464 |
| 2 | 0.034241 | 64.0 | 78.75 | 4.243464 |
| 3 | 0.042802 | 80.0 | 79.50 | 3.953900 |
| 4 | 0.051362 | 96.0 | 80.00 | 3.761642 |
| 5 | 0.059922 | 112.0 | 79.75 | 3.857695 |
| 6 | 0.068483 | 128.0 | 80.25 | 3.665737 |
| 7 | 0.077043 | 144.0 | 79.75 | 3.857695 |
| 8 | 0.085603 | 160.0 | 79.25 | 4.050260 |
| 9 | 0.094164 | 176.0 | 79.00 | 4.146780 |
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: {'num_windows': 1872, 'velocity_cv': 0.15191629119135608, 'velocity_mean': 4.023862192353049, 'velocity_median': 3.9538996252060663}
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.