API Reference
Telemed ultrasound interop: extract .tvd recordings to HDF5 + per-panel mp4.
Three named entry points – pick the one that matches what you want:
telemed.export_h5()– extract.tvd->.tvd.h5sidecars via the AutoInt1 COM API. Requires Administrator-mode EchoWave II + Administrator-mode Python (Windows-only). Network-drive aware (auto-stages via local temp because EchoWave’sOpenFilefails on UNC / mapped paths).telemed.export_video()– encode.tvd.h5->.mp4per active B-mode panel. Offline (no EchoWave needed). Lossless h265 mono by default (raw uint8 gray frames -> nothing to gain from CRF quantisation;preset="ultrafast"is the bench-validated sweet spot for both encode AND decode speed at ~15% larger files than slow). Auto-splits dual-probe recordings pern_b_images; normalises L/R-flip whenb_is_scan_direction_changedis True so cohort mp4s land in a canonical orientation.telemed.process()– end-to-end orchestrator for.tvd -> .tvd.h5 -> .mp4(s) + .dnav-toc(s). Triages sources into set A (need extraction) and set B (already have .h5); runs the appropriate pipeline(s), or both concurrently when the cohort is mixed (the COM-bound extract pipeline and the CPU/disk-bound encode-only pipeline have orthogonal bottlenecks). For Set A, each file’s encode + TOC + upload runs in the background while the next file’s COM extract executes, so wall time is bounded by extract alone. Returns{"h5": ..., "video": ..., "toc": ...}. Idempotent under defaultskip_existing=True.
Plus telemed.crop for the legacy mp4-crop workflow (side-by-side
EchoWave mp4 export -> per-side h265 monochrome; deprecated, removed
in v0.2.0) and telemed.Log for loading + viewing a single
.tvd.h5 sidecar (Log.view includes a depth-calibrated scale
bar; Log.to_video / Log.ensure_mp4 are single-recording
conveniences around export_video; Log.mp4_path reports where a
per-panel mp4 would land without forcing an encode).
Public surface (everything advertised here):
import telemed
# Pipeline
telemed.export_h5(source) # tvd -> tvd.h5 (Admin + EchoWave)
telemed.export_video(source) # tvd.h5 -> mp4(s) (offline)
telemed.process(source) # = export_h5 + export_video
# Keep the COM extract at full speed when backgrounded / RDP-disconnected
telemed.keep_full_speed() # opt Python + EchoWave out of EcoQoS
# Completeness QC (catch EchoWave memory-truncated extractions)
telemed.verify_complete(source) # compare extracted vs .tvd-declared
telemed.backfill_tvd_n_frames(source) # add declared count to old h5s
telemed.read_tvd_n_frames(tvd) # frame count from a .tvd header
telemed.looks_lut_inverted(h5) # detect EchoWave <4.4.0 LUT bug
# Legacy mp4 cropping (deprecated; will be removed in v0.2.0)
telemed.crop_video(...)
telemed.crop_folder(...)
# Analysis
lf = telemed.Log("recording.tvd.h5")
lf.view()
lf.to_video() # encode every active panel
mp4 = lf.ensure_mp4(panel=2) # encode-if-missing, return path
lf.mp4_path(panel=1) # plan-only, no I/O
lf.frame(0, crop=True, panel=2) # per-panel cropped frame
Extract (.tvd → .tvd.h5)
COM-backed reader for Telemed .tvd (Telemed Video Data) files.
Wraps the AutoInt1 automation interface that ships with EchoWave II
(see C:/Program Files/Telemed/Echo Wave II Application/EchoWave II/
Config/Plugins/AutoInt1Client.txt for the underlying API docs).
This is the only chroma-free + native-VFR-timing path for Telemed device data: bypasses the lossy mp4 export entirely, gives true uint8 grayscale arrays and per-frame timestamps at the device’s native ~100 ns precision. (DICOM exports also carry the timing in FrameTimeVector, but truncate above ~10k frames – unusable for typical pia02 recordings.)
One-time setup (per machine):
Register the COM ProgID – open an Administrator PowerShell:
cd "C:\Program Files\Telemed\Echo Wave II Application\EchoWave II\Config\Plugins" .\AutoInt1_regasm.bat
You should see “Types registered successfully”.
Per-session setup:
Start Echo Wave II as administrator (right-click -> “Run as administrator”). Get it to its normal main window.
Run Python from an Administrator shell – the COM connection only binds when both processes share elevation.
Network-drive note: EchoWave’s OpenFile fails on UNC / mapped network
paths in our setup. extract_recording_folder() handles this
transparently by copying each source file to a local temp directory,
processing locally, and writing results back to the source folder.
Example:
import telemed
# Single file -- writes a sibling .tvd.h5
telemed.export("C:/data/some.tvd")
# Timing-only (much faster; skip pixel extraction)
telemed.export("C:/data/some.tvd", frames=False)
# Batch a folder, even when on a network drive
telemed.export("M:/data/pia02")
# Mix folders and individual files
telemed.export(["M:/data/pia02", "M:/data/pia03", "C:/scratch/x.tvd"])
Known win32com gotcha (wrapped inside this module): zero-argument COM
methods on the .NET CCW are exposed as properties, not callables.
Attribute access invokes the call. See
feedback_win32com_dotnet_ccw_zero_arg_property in auto-memory.
- telemed._extract.read_tvd_n_frames(tvd_path, *, _header_bytes=65536)
Recorded frame count declared in a
.tvdcontainer header.Parses the RIFF-like “UIFF” header (64-bit chunk sizes; see the module-level comment) and returns the frame count from the first per-stream
strhchunk. This is the count the device wrote, independent of EchoWave’s memory-limited load – compare it against the extractedn_framesto detect a truncated extraction (telemed.verify_completedoes this for you).Only the first
_header_bytesof the file are read (the headers live in the first few KB), so this is cheap even on a 20 GB recording on a network drive.- Returns:
The declared frame count, or
Noneif the file isn’t a readable.tvd(wrong magic, nostrhchunk in the header window, truncated header). The value runs ~2 frames above EchoWave’sGetFramesCounton complete recordings, so treat it as a tolerance reference, not an exact one.- Parameters:
tvd_path (str | Path) –
_header_bytes (int) –
- Return type:
int | None
- class telemed._extract.TelemedRoi(img_id, x1, x2, y1, y2, width, height, physical_dx_cm_per_px, physical_dy_cm_per_px)
B-mode region-of-interest within the full Telemed display frame.
img_idfollows the AutoInt1Client convention: 1=B, 2=B2, 3=B3, 4=B4 (additional B-mode panels light up when a second / third / etc. probe is active or a multi-image scan mode is in use).physical_dx / dyare cm/pixel for this panel – per-axis spatial calibration, which can differ between panels if probes have different geometries.Units of
x1/x2/y1/y2are pixels in the full-frame display coordinate system. Note the COM API uses 1-based pixel indexing (so x1=73 means the ROI’s leftmost column is the 73rd pixel from the left edge);widthandheightare inclusive pixel counts (x2 - x1 + 1,y2 - y1 + 1).- Parameters:
img_id (int) –
x1 (int) –
x2 (int) –
y1 (int) –
y2 (int) –
width (int) –
height (int) –
physical_dx_cm_per_px (float) –
physical_dy_cm_per_px (float) –
- classmethod from_cmd(cmd, img_id=1)
Probe one img_id;
Noneif the panel isn’t active.AutoInt1’s Get* calls behave inconsistently for inactive panels: - Sometimes they raise (we catch). - Sometimes they return the zero-rect sentinel
(0,0,0,0)(observed 2026-05-24 on the usl02 single-probe probe – img_ids 2/3/4 all came back as
(0,0,0,0)rather than raising).Inverted or negative-coordinate rectangles also indicate ‘not present’.
We reject anything that isn’t a strict positive-area rectangle.
- Parameters:
img_id (int) –
- Return type:
TelemedRoi | None
- class telemed._extract.TelemedRecordingMeta(n_frames, full_frame_width, full_frame_height, b_mode_rois, image_dx_cm_per_px, image_dy_cm_per_px, source_tvd_path, extracted_at_iso, schema_version='v1', params=<factory>)
Per-recording metadata captured alongside per-frame timing.
Persisted into the HDF5 sidecar’s root attributes so downstream code can reproduce crops + scale physical measurements without re-opening the .tvd through EchoWave.
Schema v2 (2026-05-23) added an opportunistic
paramsdict populated via_PARAM_SPECS– best-effort probe / beamformer identity + cine-end timestamp + B-mode acquisition settings. The keys are pre-prefixed ("param_..."); failed probes are absent.Schema v1a3 (2026-05-24, formerly v3) expanded
_PARAM_SPECSwith B-mode geometry / orientation probes (scan-direction-changed, rotate, view-area, scan-type, steering angle, lines density, zoom), pixel-semantics gaps (power, rejection, palette ID, palette- negative, dynamic-focus, enhancement / speckle-reduction levels alongside the existing enable bools), and sanity probes (file-opened, scanning-state, probe-active). Lets cross-machine cohort audits flag silent geometry mismatches.Schema v1a4 (2026-05-24, formerly v4) replaced the single
b_mode_roiwith a dict of ROIs keyed byimg_id(1=B, 2=B2, 3=B3, 4=B4) so dual-probe / multi-image recordings can be split losslessly at the encode step. Pixel-resolution lifted into each ROI (per-axis cm/px can differ between panels when probes have different geometries). Root attrs gainedn_b_images(count) androi{N}_*/physical_d{x,y}{N}_cm_per_pxblocks per active img_id.Schema v1a5 (2026-05-24) adds display-scale root attrs
image_dx_cm_per_pxandimage_dy_cm_per_px: cm-per-pixel derived fromb_depth_mm / 10 / panel_height_pxper Telemed support’s “trust the depth setting” calibration. These are the scales downstream measurement code should use for tracked-point cm conversion (physical_d{x,y}{N}_cm_per_pxis the beamformer-native scale, kept for hardware provenance but ~2% off the display scale on typical acquisitions). Global per recording (not per-img_id), because the depth knob is global in EchoWave and the square-pixel display assumption holds across all probed Telemed configurations.Inner-image autocrop is intentionally NOT in the schema. The encoder detects the inner ultrasound image (depth ruler, side margins, bottom-tick row stripped) from frame pixels at encode time, not extract time. Keeping the autocrop bounds out of the sidecar means a detector tweak only requires a re-encode (offline), not a re-extract (Admin EchoWave). See
_encode._detect_image_roifor the algorithm; the result is deterministic given the same panel pixels.Versioning:
schema_versionis a string. During pre-release iteration it carries an alpha suffix ("v1aN"); at public release it collapses to"v1".Logaccepts both the current string form and legacy integer values (1-4 = v1a1-v1a4) via the back-compat path.- Parameters:
n_frames (int) –
full_frame_width (int) –
full_frame_height (int) –
b_mode_rois (dict[int, telemed._extract.TelemedRoi]) –
image_dx_cm_per_px (float | None) –
image_dy_cm_per_px (float | None) –
source_tvd_path (str) –
extracted_at_iso (str) –
schema_version (str) –
params (dict[str, Any]) –
- property n_b_images: int
Number of active B-mode panels (1 single-probe, 2 dual-probe).
- to_flat_attrs()
Flatten for HDF5 root-attribute persistence (no nested dicts).
Per-ROI fields expand to
roi{N}_x1/.../physical_dx{N}_cm_per_pxblocks;paramsis merged in as-is (keys already carry theparam_prefix);image_d{x,y}_cm_per_pxare skipped if None.- Return type:
dict
- class telemed._extract.TelemedTvdReader
Wraps the EchoWave II AutoInt1 COM interface.
A single reader instance is fine for the lifetime of the EchoWave process;
open()can be called repeatedly to switch files (Echo Wave is single-document, so the previously-open file is closed implicitly).See module docstring for setup prereqs.
- connect()
Attach to the running Echo Wave II instance via the COM ROT.
- Raises:
RuntimeError – If GetActiveObject fails (Echo Wave not running, COM ProgID not registered, or elevation mismatch).
- Return type:
- open(tvd_path)
Open a .tvd file in Echo Wave (idempotent freeze/stop).
Stops any running scan, recording, or cine playback before opening (per AutoInt1Client.txt those states prevent GoToFrame1n navigation).
- Raises:
FileNotFoundError – If
tvd_pathdoesn’t exist.RuntimeError – If EchoWave’s OpenFile returns -1 (common cause: the file is on a network drive – EchoWave’s OpenFile fails on UNC / mapped network paths in our setup, so callers should copy to local first).
- Parameters:
tvd_path (str | Path) –
- Return type:
- property b_mode_rois: dict[int, telemed._extract.TelemedRoi]
All active B-mode ROIs keyed by img_id (1=B, 2=B2, …).
- property b_mode_roi: TelemedRoi
the primary (img_id=1) B-mode ROI.
Equivalent to
self.b_mode_rois[1]. RaisesKeyErrorif img_id=1 is somehow not present (unexpected on any valid recording).- Type:
Convenience
- get_frame_time_ms(frame_idx_0n)
Time of frame
frame_idx_0nin ms, with frame 0 -> 0.0.frame_idx_0nis 0-indexed for Python convention; the underlying COM API is 1-indexed and the conversion happens here.- Parameters:
frame_idx_0n (int) –
- Return type:
float
- get_frame_gray(frame_idx_0n)
Get uint8 grayscale pixel array for the given frame.
Returns a numpy array of shape (H, W) covering the FULL Echo Wave display, not the B-mode ROI. Crop yourself via
b_mode_roi.- Parameters:
frame_idx_0n (int) –
- extract_metadata()
Snapshot per-recording metadata for sidecar persistence.
- Return type:
- telemed._extract.connect()
Build + connect a
TelemedTvdReaderin one call.- Return type:
- telemed._extract.export_h5(source, *, recursive=True, pattern='*.tvd', skip_existing=True, frames=True, compression='gzip', compression_opts=4, copy_to_local=None, local_temp_root=None, keep_full_speed=True, postprocess=None, progress=True, progress_callback=None, cancel_check=None)
Extract Telemed
.tvdrecording(s) to HDF5 sidecar(s).Stage 1 of the canonical pipeline (
.tvd -> .tvd.h5 -> .mp4). Requires Administrator-mode EchoWave II + Administrator-mode Python. The encode stage (export_video) is offline and runs in any Python. The compositeprocess()does both in one call.sourcemay be:A path to a single
.tvdfile.A directory (walked for
patternfiles;recursive=Trueby default).An iterable of any combination of the above.
For each .tvd, a sibling
<stem>.tvd.h5is written. Withskip_existing=True(default), files whose sidecar already exists are skipped, so re-running on a partly-processed corpus picks up where the previous run left off.Opens one Echo Wave COM connection for the entire job (cheap per-file overhead).
Network-drive handling. EchoWave’s
OpenFilefails on UNC / mapped-network paths. Whencopy_to_localis True (orNone– the default – and the source looks like a network path: non-C: drive letter, or starts with\\), each source is copied to a unique subdir oflocal_temp_root(default: system temp dir), processed there, and the resulting HDF5 is copied back next to the original.tvd. The local staging dir is cleaned up after each file (even on error).- Parameters:
source (str | Path | Iterable[str | Path]) – File path, directory, or iterable of either. See above for shape semantics.
recursive (bool) – If True (default), recurse into subdirectories when walking directories. Ignored for individual file entries.
pattern (str) – Glob for file selection inside walked directories (default
"*.tvd"). Ignored for individual file entries.skip_existing (bool) – If True (default), skip files whose
.tvd.h5sidecar already exists at the destination.frames (bool) – If True (default), include raw grayscale frames in the HDF5 sidecar. Pass False for a fast timing-only extraction (~3x faster, much smaller output).
compression (str) – HDF5 compression for the frames dataset.
"gzip"(default) is lossless and ~5x smaller for typical ultrasound;"lzf"is faster but ~30% larger;Noneskips compression entirely.compression_opts (int) – gzip level [0-9]; default 4.
copy_to_local (bool | None) – Force-on/off the network-aware copy.
None(default) auto-detects per source path.local_temp_root (str | Path | None) – Where to stage local copies.
Noneuses the system temp directory.keep_full_speed (bool) – If True (default), opt the Python process and the running
EchoWave.exeout of Windows background power throttling (EcoQoS) before extracting, and inhibit system sleep, so the ~5 fps rate holds even when the driving console is backgrounded or the RDP session is disconnected. Best-effort and no-op off Windows; seetelemed.keep_full_speed().postprocess (Callable[[_StagedFile, bool], None] | None) – Optional hook called on a background worker after every COM extract. Signature
fn(staged: _StagedFile, success: bool) -> None; invoked withsuccess=Truewhen the extract wrote the local .h5,Falsewhen it raised. Default (None) is the legacy “upload .h5 + cleanup local temp” behaviour. The dispatcher (telemed.process()) replaces this with a richer hook that also encodes mp4s, uploads them, and builds the dnav TOC sidecar – so the cost of encode + TOC hides inside the COM extract window of the next file. Custom hooks must not raise; they own their own error reporting.progress (bool) – If True (default), print
[i/N] <filename>before each file and let the per-file tqdm bar render. False suppresses both.progress_callback (Callable[[int, int, Path, str], None] | None) – Optional
fn(idx, total, path, status)for machine-readable progress – matches thedustrack.batchconvention.cancel_check (Callable[[], bool] | None) – Optional zero-arg callable polled between files. If truthy, the loop exits early; the partial results dict is returned; any in-flight local staging dir is cleaned up.
- Returns:
{path: status}where status is"built"(just extracted),"hit"(skipped existing), orf"error: {msg}".- Return type:
dict
Examples:
# One file telemed.export("C:/data/scan.tvd") # One folder (recursive walk for *.tvd) telemed.export("M:/data/pia02") # Mix of folders and individual files telemed.export([ "M:/data/pia02", "M:/data/pia03", "C:/scratch/single.tvd", ]) # Timing only -- fast pass for bulk metadata extraction telemed.export("M:/data/pia02", frames=False)
Encode (.tvd.h5 → .mp4)
Encode Telemed .tvd.h5 sidecars into mp4 video files.
Consumed by DUSTrack / DLC etc., but the output is just an mp4 – this module is named for what it does, not the downstream tool.
Public surface (re-exported from telemed):
telemed.export_video(source) # file | folder | list -> mp4(s)
telemed.Log("rec.tvd.h5").to_video() # single-recording convenience
Dispatcher convenience:
telemed.export(source, kind="h5") # tvd -> tvd.h5 (default)
telemed.export(source, kind="video") # tvd.h5 -> mp4(s)
telemed.export(source, kind="both") # tvd -> tvd.h5 -> mp4(s)
Design choices baked in here:
Lossless by default. The source frames in the .tvd.h5 are uint8 grayscale straight off the device; there’s no upstream lossy step to reclaim quality from, so a CRF-tuned encode is buying file size at the cost of DLC accuracy that the device gave us for free. The cropped ROI is small enough that lossless h265 mono is tolerable – a typical 20k-frame pia02 recording lands at ~1-2 GB. Pass
lossless=Falsewith an explicitcrf=if you want to trade a few percent accuracy for ~50x smaller files.Per-panel split for multi-image recordings. If the sidecar has
n_b_images > 1(dual-probe scans), we write one mp4 per active img_id –<stem>_b{N}.mp4– each cropped to its own ROI. Single- probe stays<stem>.mp4.Autocrop to the inner ultrasound image. The AutoInt1-reported panel ROI is the full B-mode panel (depth ruler, side margins, bottom-tick row, inner image). The encoder detects the inner image by content (gray-margin step + bottom tick-row peel) from a 16-frame mean of the sidecar’s
/frames/grayand crops to that box by default (crop="image"); recordings where detection can’t identify the inner box fall back to the panel ROI with a warning.crop="panel"opts out explicitly. The autocrop bounds are NOT persisted in the sidecar – detection happens at encode time so detector tweaks only need a re-encode, not a re-extract.Orientation normalisation. When
b_is_scan_direction_changedis True on the sidecar, the output is L/R-flipped during encode so every cohort mp4 lands in a canonical orientation regardless of which EchoWave operator toggled scan direction. U/D flip has no API getter so cannot be detected; rotation handling is deferred until we see a recording with non-zerob_rotate.No timing CSV. The .tvd.h5 already carries /timing/time_ms + /timing/ifi_ms; downstream consumers that need real time round-trip via
telemed.Log(<stem>.tvd.h5).time_ms[frame_idx]. The mp4 declares CFR at the recording’s mean fps so DLC / cv2 index by frame number unchanged.
- telemed._encode.export_video(source, *, recursive=True, pattern='*.tvd.h5', out_dir=None, codec='h265_mono', lossless=True, crf=24, preset='ultrafast', fps=None, normalize_orientation=True, crop='image', skip_existing=True, overwrite=False, build_toc=True, progress=True, progress_callback=None, cancel_check=None)
Encode Telemed
.tvd.h5recording(s) to.mp4file(s).Single unified entry point.
sourcemay be:A path to a single
.tvd.h5file (encoded as-is).A path to a
.tvdfile (its sibling.tvd.h5is encoded; missing sidecar -> silently skipped).A directory (walked for
pattern– default*.tvd.h5).An iterable of any combination of the above.
Per recording,
n_b_imagesoutputs are written: single-probe -><stem>.mp4, multi-probe (n>=2) -><stem>_b{img_id}.mp4per active panel.- Parameters:
source (str | Path | Iterable[str | Path]) – File path, directory, or iterable of either.
recursive (bool) – When True (default), recurse into subdirectories during
patternwalk.pattern (str) – Glob filter for directory walks. Default
"*.tvd.h5".out_dir (str | Path | None) – Output directory.
None(default) co-locates each mp4 next to its source.tvd.h5.codec (str) – Output codec preset.
"h265_mono"(default).lossless (bool) – When True (default), produce a lossless h265 mono encode (
-x265-params lossless=1). False usescrf. See module docstring for rationale (raw uint8 gray source -> nothing to gain from CRF quantisation).crf (int) – ffmpeg CRF for the lossy branch. Default 24.
preset (str) – ffmpeg
-presetvalue. Default"ultrafast"(lossless bit-exact regardless of preset;ultrafasttrades ~15% larger files for ~7x faster encode + ~2.5x faster decode vs"slow"). Pass"slow"to reclaim the smallest lossless files. See module docstring for the bench numbers backing the default.fps (float | None) – CFR fps declared in the mp4 container.
None(default) uses each recording’smean_fps. Real per-frame timing stays in the.tvd.h5(/timing/time_ms).normalize_orientation (bool) – When True (default), L/R-flip the output if the sidecar reports
b_is_scan_direction_changed=Trueso every cohort mp4 lands in a canonical orientation. False ships pixels as-stored in the sidecar.crop (str) –
"image"(default) crops each output to the inner ultrasound image (depth ruler / side margins / bottom-tick row stripped) when the sidecar carries v1a6 inner-ROI fields; falls back to the outer panel with a warning on legacy sidecars."panel"keeps the outer B-mode panel (the pre-v1a6 behaviour) for inspection / debugging.skip_existing (bool) – When True (default), per-output: skip if the target
.mp4already exists. (Each panel checked independently for multi-probe sidecars.)overwrite (bool) – When True, clobber any existing target
.mp4. Mutually exclusive withskip_existing=True; if both,overwritewins.build_toc (bool) – When True (default), build the
<mp4>.dnav-tocsidecar after each successful encode (and rebuild a missing sidecar on the skip-existing path) sodnav.VideoReader/ DUSTrack open the mp4 without the cold-open demux pass. Silently skipped (with a one- time warning) whendatanavigatorisn’t importable. False suppresses the sidecar build entirely.progress (bool) – When True (default), print a line per panel encoded AND render a per-frame tqdm bar during each panel’s encode (the bar’s
descis the output mp4 stem). False suppresses both.progress_callback (Callable[[int, int, Path, str], None] | None) – Optional
fn(idx, total, mp4_path, status)– matches thedustrack.batchconvention.idx/totalare per-PANEL counts, not per-recording.cancel_check (Callable[[], bool] | None) – Zero-arg callable polled between panels; truthy -> exit early.
- Returns:
{mp4_path_str: status}where status is"built"/"hit"(skipped existing) /f"error: {msg}".- Return type:
dict
Examples:
# One file -- writes a sibling .mp4 (or per-panel .mp4s) telemed.export_video("M:/data/scan.tvd.h5") # One folder (recursive walk for *.tvd.h5) telemed.export_video("M:/data/pia02") # Lossy branch telemed.export_video("M:/data/pia02", lossless=False, crf=22)
Pipeline orchestrator
Pipeline orchestrator for telemed.process().
Folds the three pipeline stages (.tvd -> .tvd.h5 -> .mp4 + .dnav-toc)
into a single per-file end-to-end pipeline so the encode + TOC + upload
cost for file N hides inside the COM extract window of file N+1.
Bottleneck shape on the typical pia02 workload (20k-frame dual-probe recording on a network drive):
Extract (COM, single-threaded): ~33 min (timing-only) / ~67 min (pixels)
Encode mp4(s) (libx265 ultrafast): ~100 s (dual panel)
TOC build (PyAV demux): ~80 s (dual panel)
Upload (h5 ~20 GB + mp4s ~3 GB + TOCs): ~3-5 min
So the full post-process for one recording (~6-9 min) comfortably hides inside the next recording’s extract (~33-67 min). The user’s wall clock is bounded by extract time + a small drain at the end.
Triage shape:
Set A:
.tvdfiles with no sibling.tvd.h5(need extraction)Set B:
.tvd.h5files already on disk (just need encode + TOC)
The dispatcher routes to one pipeline, the other, or both. Scenario 4 (both non-empty) runs the two pipelines concurrently on a top-level 2-thread executor; the bottlenecks are orthogonal (Set A is COM-bound, Set B is CPU/disk-bound for libx265 + PyAV) so they don’t compete on the critical path.
- telemed._dispatch.process(source, **kwargs)
Run the full pipeline –
.tvd -> .tvd.h5 -> .mp4(s) + .dnav-toc.The end-to-end orchestrator for the canonical Telemed workflow. Triages sources into two sets and runs the appropriate pipeline for each; when both sets are non-empty, runs them concurrently on a 2-thread executor.
Set A (
.tvdwithout sibling.tvd.h5): full pipeline. COM extract on the main thread; for each completed extract, a background worker encodes mp4(s), uploads everything to the source folder (when the source is on a network drive), and builds the dnav TOC sidecar. The post-process cost hides inside the next file’s extract window.Set B (
.tvd.h5already on disk): sequential encode + TOC. No COM, no upload. Idempotent on cohorts where mp4 + TOC already exist (skip_existing=True).
Per-stage idempotency: re-running on a partly-processed corpus picks up where the previous run left off. Existing
.tvd.h5skips extract; existing.mp4skips encode; existing.dnav-tocskips TOC build. Force a full rebuild withskip_existing=False, overwrite=True.Kwargs are signature-routed:
h5-only kwargs (
frames,compression,compression_opts,copy_to_local,local_temp_root) -> Pipeline A’sexport_h5.video-only kwargs (
out_dir,codec,lossless,crf,preset,fps,normalize_orientation,crop,build_toc,overwrite) ->export_videoin both pipelines.Common kwargs (
recursive,skip_existing,progress,progress_callback,cancel_check) -> both pipelines.
- Returns:
{"h5": {...}, "video": {...}, "toc": {...}}– per-stage result dicts. Each maps path-string -> status ("built"/"hit"/"missing"/"skipped: no dnav"/f"error: {msg}").- Parameters:
source (str | Path | Iterable[str | Path]) –
- Return type:
dict
Example:
telemed.process(r"M:/data/pia02") # Walks for .tvd + .tvd.h5; triages into needs-extract vs # has-h5; runs both pipelines concurrently if both sets # are non-empty; returns when every file has its mp4 + TOC.
Log — analysis entry point
Log – entry point for analysis of an exported Telemed recording.
Loads the HDF5 sidecar produced by telemed.export_h5().
Construct with a single file path, get typed attributes for the data
plus small methods that do the typical analysis / inspection work
directly on the instance.
Example:
import telemed
lf = telemed.Log("M:/data/054/telemed/scan.tvd.h5")
print(lf.n_frames, lf.duration_s, lf.b_mode_roi)
lf.view() # interactive frame browser
img = lf.frame(0) # uint8 H x W
cropped = lf.frame(0, crop=True) # uint8 roi_h x roi_w
Frame data is read lazily from the HDF5 (random-access; no need to load 20k frames just to peek at one).
- class telemed.log.Roi(img_id, x1, x2, y1, y2, width, height, physical_dx_cm_per_px, physical_dy_cm_per_px)
B-mode region-of-interest, mirroring the export-side dataclass.
Stored as immutable so callers can pass it around without accidental mutation. The slice helpers are convenient when indexing into a full-frame numpy array.
img_idfollows the AutoInt1 convention (1=B, 2=B2, 3=B3, 4=B4);physical_d{x,y}_cm_per_pxis per-panel since multi-probe recordings can have different physical resolutions per transducer.The Roi describes the outer B-mode panel from AutoInt1 (depth ruler + side margins + inner image). The inner-ultrasound- image sub-rectangle (depth ruler / margins / tick row stripped) is computed on demand from frame pixels – see
Log.image_roi().- Parameters:
img_id (int) –
x1 (int) –
x2 (int) –
y1 (int) –
y2 (int) –
width (int) –
height (int) –
physical_dx_cm_per_px (float) –
physical_dy_cm_per_px (float) –
- as_slice()
Return
(y_slice, x_slice)for the outer panel ROI.Telemed’s COM API uses 1-based pixel indexing; we convert to 0-based Python slices here. End points are inclusive in the source convention, so the slice end gets +1.
- Return type:
tuple
- class telemed.log.Log(fname)
Load a Telemed
.tvd.h5sidecar.- Parameters:
fname (Union[str, os.PathLike]) – Path to the HDF5 sidecar (
<stem>.tvd.h5) produced byextract_recording().- Variables:
fname (Path) – Full HDF5 path passed in.
name (str) – File stem (extensions stripped) for use as a recording identifier.
n_frames (int) – Number of frames in the recording.
full_frame_height (full_frame_width /) – Pixel dims of the Echo Wave display frames stored in
/frames/gray.b_mode_rois (dict[int, Roi]) – All active B-mode ROIs keyed by
img_id(1=B, 2=B2, …). Single-probe recordings get{1: Roi(...)}; dual-probe (B+B2 side-by-side) gets{1: ..., 2: ...}. Each Roi carries its ownphysical_d{x,y}_cm_per_px(per-panel calibration). v1-v3 sidecars collapse to{1: ...}.n_b_images (int) – Count of active B-mode panels (1 for single- probe, 2+ for multi-probe / multi-image).
b_mode_roi (Roi) – Backward-compat alias for
b_mode_rois[1].physical_dy_cm_per_px (physical_dx_cm_per_px /) – Backward-compat aliases for the img_id=1 panel’s per-axis spatial resolution.
time_ms (np.ndarray) – Absolute time of each frame in ms, with frame 0 -> 0.0. Shape
(n_frames,).ifi_ms (np.ndarray) – Inter-frame intervals in ms.
ifi_ms[0]is 0 (frame 1 anchor). Shape(n_frames,).source_tvd_path (str) – Path the data was extracted from.
extracted_at_iso (str) – When the HDF5 was written.
tvd_declared_n_frames (Optional[int]) – Frame count recorded in the source
.tvdcontainer header, stored at extract time.Noneon sidecars extracted before the completeness-QC feature. When this sits well aboven_frames, EchoWave truncated the load to fit memory – audit a cohort withtelemed.verify_complete.schema_version (str) – HDF5 schema version. Always reports
"v1"– production extracts write that label and Log also normalises the legacy in-development variants ("v1a1"through"v1a5", or integers1..4) to"v1"on load. The historical labels reflect what each iteration added (single-ROI -> ParamGet sweep -> expanded ParamGet -> per-img_id multi-ROI -> stored display-scale) and are documented in the changelog; downstream code should branch on the data, not the label.params (dict[str, Any]) – Per-recording acquisition parameters captured at export time via the AutoInt1 ParamGet* interface (schema v2+). Keys are short (no
param_prefix); use.get(name)since failed-probe params are absent. Common keys when populated:probe_name/probe_code,beamformer_name/beamformer_code,cine_end_datetime_str, B-mode acquisitionb_depth/b_frequency/b_gain/b_power/b_dynamic_range/b_focus_depth/b_focuses_count/b_is_dynamic_focus/b_thi/b_frame_averaging/b_rejection/b_image_enhancement{,_method}/b_speckle_reduction{,_level}/b_palette/b_palette_{gamma,brightness,contrast,negative}, geometry / orientationb_is_scan_direction_changed(L/R flip) /b_rotate/b_view_area/b_scan_type/b_steering_trapezoid_angle/b_lines_density/b_zoom_factor, sanityis_usg_file_opened/scanning_state/is_probe_active. Empty{}on schema v1 sidecars.
Notes
Frame data is loaded lazily (random-access via h5py). The timing and metadata arrays ARE eagerly loaded on construction because they’re tiny.
- property duration_s: float
Recording duration in seconds (last frame’s absolute time).
- property mean_fps: float
Implied average fps from the recording duration.
- property has_frames: bool
True if the HDF5 has pixel data (i.e. wasn’t extracted with frames=False).
- property physical_dx_cm_per_px: float
Per-pixel horizontal cm of the img_id=1 panel.
This is the beamformer-native spacing reported by AutoInt1’s
GetUltrasoundPhysicalDeltaX. It does NOT match the on-screen display scale (the two differ by ~2% on typical Telemed acquisitions). For pixel-to-cm conversion in tracked- point analysis, preferimage_dx_cm_per_px.
- property physical_dy_cm_per_px: float
Per-pixel vertical cm of the img_id=1 panel.
Beamformer-native spacing – see notes on
physical_dx_cm_per_px. For measurements, useimage_dy_cm_per_px.
- property image_dy_cm_per_px: float | None
Display vertical scale – cm per panel pixel.
Schema v1a5+ stores this as a root attr (computed at extract time as
b_depth_mm / 10 / panel_height_pxper Telemed support’s “trust the depth setting” calibration). Legacy sidecars (v1a1..v1a4) lack the stored value; this property derives it on the fly when possible. ReturnsNonewhen the sidecar has nob_depthparam either (v1a1 schemas).Use this for cm conversions on tracked-point coordinates –
physical_dy{N}_cm_per_pxis the beamformer-native scale, kept for hardware provenance but ~2% off the display scale on typical Telemed acquisitions.
- property image_dx_cm_per_px: float | None
Display horizontal scale – cm per panel pixel.
Telemed renders square display pixels (1:1 aspect, so anatomy isn’t squished), and AutoInt1 reports
physical_dx == physical_dyfor all probed acquisitions. So the display x scale equalsimage_dy_cm_per_px. If a future probe is found to break this assumption it would surface as anatomy rendered with a non-1:1 aspect inview(); revisit then.Stored as a root attr in v1a5+; back-compat fallback via
image_dy_cm_per_pxfor legacy sidecars.
- frame(frame_idx_0n, *, crop=False, panel=1)
Read a single frame as uint8.
- Parameters:
frame_idx_0n (int) – 0-indexed frame number.
crop (bool | str) –
False(default) -> full Echo Wave display frame.Trueor"image"-> the inner ultrasound image (detected from frame pixels; depth ruler / side margins / bottom-tick row stripped). Falls back to the outer panel ROI when the detector can’t identify the inner box."panel"-> the outer B-mode panel ROI (depth ruler + margins included).panel (int) –
img_idof the panel to crop to (1=B, 2=B2, …). Defaults to 1 so single-probe call sites work unchanged. Validated even whencrop=Falseso a typo doesn’t silently pass.
- Returns:
np.ndarrayof shape(H, W)– full frame or cropped depending oncrop.- Raises:
RuntimeError – If the HDF5 was written without frames (
extract_recording(..., frames=False)).IndexError – If
frame_idx_0nis out of range.KeyError – If
panelis not an active img_id.ValueError – If
cropis not bool,"image", or"panel".
- Return type:
ndarray
- image_slice(panel=1)
(y_slice, x_slice)for the inner ultrasound image panel.Runs the content-based detector on a multi-frame mean of the panel and caches the resulting bounds on this Log instance. Returns the outer panel slice when the detector can’t identify the inner box (warns once per panel).
- Parameters:
panel (int) –
- Return type:
tuple
- mp4_path(panel=1, *, out_dir=None)
Where the per-panel mp4 would land for this recording.
Deterministic from
n_b_images+ the chosenout_dir. Mirrorsexport_video’s naming so downstream callers don’t recreate the convention:single-probe ->
<stem>.mp4multi-probe ->
<stem>_b{panel}.mp4
Does NOT check whether the file exists – pair with
ensure_mp4()to encode if missing.- Parameters:
panel (int) –
img_idof the panel (1=B, 2=B2, …). Must be an active panel inb_mode_rois.out_dir (str | PathLike | None) – Output directory.
None(default) co-locates next toself.fname– matchesto_video()’s default.
- Raises:
KeyError –
panelis not an active img_id.- Return type:
Path
- ensure_mp4(panel=1, *, out_dir=None, **encode_kwargs)
Return the per-panel mp4 path, encoding it if missing.
Idempotent: a second call is a file-existence check, not a re-encode. The encode pass writes every active panel of the recording in one HDF5-open pass, so
ensure_mp4(1)on a dual-probe recording also produces panel 2’s mp4 as a side effect (and vice versa).- Parameters:
panel (int) –
img_idof the panel to return.out_dir (str | PathLike | None) – Output directory.
None(default) co-locates withself.fname. Forwarded to both the existence check and the encode pass so they agree on where to look.**encode_kwargs – Forwarded to
export_video–lossless/crf/preset/fps/normalize_orientation/overwriteetc. Seeexport_video.
- Returns:
Path to the per-panel mp4; guaranteed to exist on return.
- Return type:
Path
- to_video(out_dir=None, **kwargs)
Encode this recording as mp4 file(s).
Thin convenience wrapper around
telemed.export_video()operating on this sidecar’s path. Single-probe -><stem>.mp4; dual-probe -><stem>_b{img_id}.mp4per active panel. Lossless h265 mono by default; orientation normalised to canonical.- Parameters:
out_dir (str | PathLike | None) – Output directory.
None(default) co-locates the mp4(s) next toself.fname.**kwargs – Forwarded to
export_video–lossless,crf,preset,fps,normalize_orientation,overwrite, etc. Seeexport_videodocstring.
- Returns:
{mp4_path_str: status}(one entry per panel encoded).- Return type:
dict
- view(*, crop=True, frame_idx_0n=0)
Interactive frame browser using matplotlib.
Opens a window with the current frame + a slider for scrubbing and left/right arrow-key bindings for single-frame steps. Returns the matplotlib
Figureso the caller can keep a reference (or callplt.show()afterwards in non-interactive backends).- Parameters:
crop (bool) – If True (default), show the B-mode ROI only. If False, show the full Echo Wave display frame.
frame_idx_0n (int) – Initial frame to display.
Legacy mp4 crop (deprecated)
Telemed ultrasound video helpers (legacy EchoWave-mp4 workflow).
Deprecated since version 0.1.0: The crop_video / crop_folder helpers in this module operate
on the legacy EchoWave-mp4-export workflow (side-by-side mp4 from
the Telemed device’s built-in export, cropped per-side at hardcoded
coordinates). They are superseded by the .tvd direct-read
pipeline (telemed.export_video() / telemed.process())
and will be removed in telemed v0.2.0.
Telemed MP4 recordings combine a left- and right-side ultrasound view
side-by-side in one frame. crop_video splits one such MP4 into the
two per-side clips using the lab’s standard 706x558 crop windows at
x=777 (left) / x=72 (right), y=42. crop_folder walks a study data
tree and crops every Telemed MP4 found.
Crop geometry is hardcoded — it reflects the Telemed device’s frame layout, not a per-study parameter. If the Telemed acquisition settings change (resolution, layout, overlay placement) the constants below need re-derivation.
Encoder default since 2026-05-23 is h265 4:0:0 monochrome (mono=True):
the crop output drops chroma planes entirely (-c:v libx265 -pix_fmt
gray -crf 24 -an), which fixes the chroma-noise-into-DLC-inference
penalty the older yuv420p crops carried (the bench at
S:/_corpus/telemed/_bench/ showed the yuv420p path was costing
~0.7 px median / 1.9 px p95 DLC keypoint error vs lossless). Sub-pixel
median preserved at CRF 24 (0.47 px vs lossless); p95 1.29 px. Drops
audio along with chroma.
Pass mono=False to fall back to the pre-graduation libx264
yuv420p invocation (-c:v libx264 -preset slow, no explicit -crf).
A past cv2/decord frame-extraction inconsistency on training-data
videos pointed tentatively at NVENC, so libx264 stayed the conservative
default during the graduation period; callers who want NVENC (or any
other ffmpeg encoder) pass it via the encoder kwarg, which routes
through the internal encoder_flags() helper. encoder is
ignored in the mono branch (libx264 cannot produce true 4:0:0).
- telemed.crop.encoder_flags(encoder, crf=28, preset='slow')
Return the
-c:v ...flag list for the named encoder.h264_nvencuses VBR with constant-quality;libx264uses CRF. Other encoders pass through with a generic preset.- Parameters:
encoder (str) –
crf (int) –
preset (str) –
- Return type:
list[str]
- telemed.crop.crop_video(src, dst, side, *, encoder=None, crf=None, preset='slow', mono=True)
Crop one Telemed MP4 to the left or right view; skip if
dstexists.- Parameters:
src – Source MP4 path.
dst – Output MP4 path. If it already exists, the call is a no-op.
side –
"left"(x=777) or"right"(x=72).encoder – ffmpeg video encoder name. Only honoured when
mono=False– in the mono branch the encoder is forced to libx265 (libx264 can’t produce true 4:0:0). Withmono=False, encoder=Noneyou get the pre-graduation invocation:-c:v libx264 -preset {preset}with no-crf. Pass an explicit encoder (e.g."h264_nvenc") to route through the internalencoder_flags()helper.crf – Quality. When
mono=True(default), defaults to 24 (median DLC pixel error 0.47 px vs lossless on interosseous_pn24-x; see bench inS:/_corpus/telemed/_bench/). Lower values (e.g. 22) buy tighter parity at larger file size; higher (e.g. 26 or 28) save more space. Whenencoderis notNone(mono=Falsebranch), defaults to 28 (consistent withvideo.py).preset – ffmpeg
-presetvalue. Default"slow".mono – True (default since 2026-05-23) encodes as h265 4:0:0 monochrome (
-c:v libx265 -pix_fmt gray) so the crop output is chroma-noise-free in one pass instead of needing a follow-updustrack.batch.convert_to_mono()step. Drops audio along with chroma. Passmono=Falseto restore the pre-graduation libx264 yuv420p path.
Deprecated since version 0.1.0: Use
telemed.export_video()ortelemed.process()against.tvdrecordings instead. Will be removed in v0.2.0.
- telemed.crop.crop_folder(data_dir, dest_dir, *, left_suffix, right_suffix, stem_split=' ', encoder=None, crf=None, preset='slow', mono=True)
Crop every
*telemed*.mp4underdata_dirintodest_dir.File discovery uses
pyfilemanager.FileManagerwithinclude="telemed"andexclude="archive"— preserved from the pre-graduation pia02 / chi01 implementations so existing on-disk layouts continue to match.Output filenames are
<stem_core><left_suffix>.mp4and<stem_core><right_suffix>.mp4wherestem_coreis the source stem split onstem_split(default: first whitespace-delimited token, which strips the device-emitted trailing description + timestamp).Default is
mono=True(h265 4:0:0 monochrome). Passmono=Falseto fall back to the pre-graduation libx264 yuv420p path; seecrop_video()for the trade-offs.Deprecated since version 0.1.0: Use
telemed.export_video()ortelemed.process()against.tvdrecordings instead. Will be removed in v0.2.0.
Metadata-id discovery probe
One-shot discovery probe for AutoInt1 file-level metadata.
Run this once on a representative saved .tvd to find every
ParamGet-able / direct-getter datum the COM interface will surrender
for a file-mode (probe-detached) recording. Output is a dict plus an
optional JSON + markdown report.
Intended audience: a human curating which fields go into the production
_PARAM_SPECS and the HDF5 sidecar schema. Not part of the
batch-export hot path; not exported from the package __init__.
Safety:
AutoInt1Client.txtwarns (lines 261-263) that calling the wrong ParamGet variant for an id can crash EchoWave, not merely raise an exception. To stay off the crash-prone paths this probe only sweeps ids whose description text in the doc names a specificParamGet*variant, plus_shift-suffixed ids (handled by ParamGetInt per the production convention proven in_PARAM_SPECS). Action-only commands (val = 0;) and untagged ids are recorded in the report but not probed – visible for human review without touching COM.The probe opens
tvd_pathin the currently-running EchoWave II instance, which closes whatever file EchoWave is currently showing. Do not run while another export is using the same EchoWave.
Example:
from telemed import _metadata_probe as mp
result = mp.probe("C:/data/temp2/one_recording.tvd")
md = mp.write_report(result, "C:/scratch/telemed_probe.json")
print(f"Wrote {md}")
- telemed._metadata_probe.parse_doc(doc_path=PosixPath('C:/Program Files/Telemed/Echo Wave II Application/EchoWave II/Config/Plugins/AutoInt1Client.txt'))
Extract every
#define id_* NUMBERand classify how to probe it.Strategies:
documented_get– description text contains an explicitParamGet{Int|Bool|Double|Float|String}(...)reference. Probed with that variant. (Floatis normalised toDouble: both return numeric and Double is higher precision.)shift_inferred– name contains_shiftand has no explicit ParamGet hint. Probed with ParamGetInt per the production convention used by_PARAM_SPECS(e.g.id_b_depth_shift= 305 reads via ParamGetInt). Reported asshift_inferredso “doc says so” stays distinguishable from “convention says so”.action_only– description containsval = 0;and no ParamGet hint. Set-only command ids; not probed.unknown– no hint at all. Not probed (crash-warning policy).
Duplicate
id_names keep the first definition (a handful are re-defined later inside different sections of the doc).- Parameters:
doc_path (str | Path) –
- Return type:
list[telemed._metadata_probe._IdEntry]
- telemed._metadata_probe.probe(tvd_path, *, doc_path=PosixPath('C:/Program Files/Telemed/Echo Wave II Application/EchoWave II/Config/Plugins/AutoInt1Client.txt'))
Sweep every file-level metadatum readable from one saved .tvd.
- Parameters:
tvd_path (str | Path) – Local .tvd to open. Must already be staged off any network drive (EchoWave can’t open UNC paths).
doc_path (str | Path) – Override the AutoInt1Client.txt location if the SDK is installed somewhere non-standard.
- Returns:
source_tvd/probed_at_iso– provenance.direct– non-ParamGet getters (frame count, frame dims, current frame idx + time after seeking to frame 1).ultrasound_regions–{label: {img_id, x1, x2, y1, y2, physical_d{x,y}_cm_per_px}}for every img_id that returns valid geometry.params–{id_name: {param_id, variant, strategy, description, value}}for every documented-get + shift-inferred id that returned a value.failed– same shape asparamsplus anerrfield for ids that the probe tried and errored on (most likely ‘only valid during a live scan’).skipped– ids that the doc-parser flagged as not safe to probe (action-only + unknown). For human review only.dim_consistency– frame 1 vs frame N dims + B-mode ROI;constantshould be True for normal B-mode recordings. False is a finding worth investigating (mid-recording depth / mode / probe change).
- Return type:
Dict with sections
- telemed._metadata_probe.write_report(result, out_path)
Write
resultas JSON plus a sibling markdown summary.JSON is the machine-readable canonical form; markdown is for human eyeballing. Returns the markdown path (the JSON path is just
out_path).- Parameters:
result (dict) –
out_path (str | Path) –
- Return type:
Path