Heart rate batch analysis¶
Once you have settled on a heart rate parameter set for one file, the natural next step is to apply the same fixed parameters across many files. That keeps results comparable across files, channels, and ROIs — a basic requirement for quantitative comparison.
This notebook shows how to:
- Load a folder of files with
AcqImageList. - Fix one heart rate detection parameter set.
- Run heart rate on every file/channel/ROI that already has a velocity analysis.
- Collect results in a single
pandasDataFrame. - Plot a quick Lomb-vs-Welch agreement summary.
In everyday use you would typically tune parameters interactively in the CloudScope GUI on one representative file, then run a batch script or notebook like this for the full dataset.
This notebook does not save results to sidecars. Add
acq.save()inside the loop if you want to persist batch results.
import pandas as pd
import plotly.express as px
import plotly.io as pio
pio.renderers.default = "notebook"
# Register analysis classes before loading files (see heart-rate-analysis.ipynb).
from acqstore.acq_image.analysis import RadonVelocityAnalysis
from acqstore.acq_image.analysis import DiameterAnalysis
from acqstore.acq_image.analysis import HeartRateAnalysis
from acqstore.acq_image import AcqImageList
from acqstore.sample_data import ensure_sample
Load files and fix parameters¶
We use the same demo-small sample as the single-file heart rate notebook. Replace ensure_sample(...) with your own folder path when adapting this workflow.
sample_folder = ensure_sample("demo-small")
acq_list = AcqImageList(str(sample_folder))
# One fixed parameter set applied to every file/channel/ROI in the batch.
HR_PARAMS = HeartRateAnalysis.get_default_detection_params()
HR_PARAMS["agree_tol_bpm"] = 30.0
print(f"files in batch: {len(acq_list)}")
files in batch: 4
Run heart rate across the list¶
For each file we find every saved radon_velocity analysis, ensure its velocity table is loaded, then run heart rate with the fixed parameter set. Results are collected as one row per (file, channel, roi_id).
def run_batch_heart_rate(acq_list, hr_params):
"""Run heart rate with fixed params on every velocity analysis in a list."""
rows = []
for acq in acq_list:
for analysis in acq.analysis_set.as_list():
if analysis.key.analysis_name != RadonVelocityAnalysis.analysis_name:
continue
if analysis.get_plot_data() is None:
continue
channel = analysis.key.channel
roi_id = analysis.key.roi_id
hr = acq.analysis_set.get_or_create(
HeartRateAnalysis.analysis_name, channel=channel, roi_id=roi_id
)
hr.set_detection_params(hr_params)
acq.analysis_set.run_analysis(hr.key)
summary = hr.result.summary
agree = summary.get("agreement") or {}
rows.append(
{
"file": acq.name,
"channel": channel,
"roi_id": roi_id,
"status": summary["status"],
"lomb_bpm": summary["lomb"]["bpm"],
"welch_bpm": summary["welch"]["bpm"],
"abs_delta_bpm": agree.get("abs_delta_bpm"),
"agree_ok": agree.get("agree_ok"),
}
)
return pd.DataFrame(rows)
results = run_batch_heart_rate(acq_list, HR_PARAMS)
results
| file | channel | roi_id | status | lomb_bpm | welch_bpm | abs_delta_bpm | agree_ok | |
|---|---|---|---|---|---|---|---|---|
| 0 | 20251030_A106_0002.oir | 0 | 1 | method_disagree | 308.336595 | 389.392705 | 81.056110 | False |
| 1 | 20251030_A106_0002.oir | 0 | 3 | ok | 440.078278 | 419.345990 | 20.732288 | True |
| 2 | 20251030_A106_0003.oir | 0 | 1 | ok | 436.555773 | 419.904969 | 16.650804 | True |
| 3 | 20251030_A106_0003.oir | 0 | 4 | ok | 436.555773 | 419.904969 | 16.650804 | True |
| 4 | 20251030_A106_0004.oir | 0 | 1 | ok | 440.782779 | 449.898181 | 9.115402 | True |
| 5 | 20251030_A106_0004.oir | 0 | 3 | ok | 440.782779 | 449.898181 | 9.115402 | True |
| 6 | 20251030_A106_0005.oir | 0 | 1 | method_disagree | 355.538160 | 269.938909 | 85.599252 | False |
| 7 | 20251030_A106_0005.oir | 0 | 3 | method_disagree | 355.538160 | 269.938909 | 85.599252 | False |
Agreement summary plot¶
Points on the diagonal mean the two methods agree. Points far from the diagonal are flagged for rejection (agree_ok=False).
fig = px.scatter(
results,
x="lomb_bpm",
y="welch_bpm",
color="agree_ok",
hover_data=["file", "roi_id", "status", "abs_delta_bpm"],
title="Batch heart rate: Lomb vs Welch (fixed parameters)",
labels={"lomb_bpm": "Lomb bpm", "welch_bpm": "Welch bpm", "agree_ok": "agree"},
)
fig.add_shape(type="line", x0=200, y0=200, x1=500, y1=500, line={"dash": "dash"})
fig.show()
n_accept = int(results["agree_ok"].fillna(False).sum())
print(f"accepted {n_accept} / {len(results)} selections")
accepted 5 / 8 selections