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.h5 sidecars 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’s OpenFile fails on UNC / mapped paths).

  • telemed.export_video() – encode .tvd.h5 -> .mp4 per 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 per n_b_images; normalises L/R-flip when b_is_scan_direction_changed is 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 default skip_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):

  1. 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:

  1. Start Echo Wave II as administrator (right-click -> “Run as administrator”). Get it to its normal main window.

  2. 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 .tvd container 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 strh chunk. This is the count the device wrote, independent of EchoWave’s memory-limited load – compare it against the extracted n_frames to detect a truncated extraction (telemed.verify_complete does this for you).

Only the first _header_bytes of 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 None if the file isn’t a readable .tvd (wrong magic, no strh chunk in the header window, truncated header). The value runs ~2 frames above EchoWave’s GetFramesCount on 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_id follows 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 / dy are cm/pixel for this panel – per-axis spatial calibration, which can differ between panels if probes have different geometries.

Units of x1 / x2 / y1 / y2 are 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); width and height are 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; None if 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 params dict 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_SPECS with 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_roi with a dict of ROIs keyed by img_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 gained n_b_images (count) and roi{N}_* / physical_d{x,y}{N}_cm_per_px blocks per active img_id.

Schema v1a5 (2026-05-24) adds display-scale root attrs image_dx_cm_per_px and image_dy_cm_per_px: cm-per-pixel derived from b_depth_mm / 10 / panel_height_px per 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_px is 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_roi for the algorithm; the result is deterministic given the same panel pixels.

Versioning: schema_version is a string. During pre-release iteration it carries an alpha suffix ("v1aN"); at public release it collapses to "v1". Log accepts 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_px blocks; params is merged in as-is (keys already carry the param_ prefix); image_d{x,y}_cm_per_px are 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:

TelemedTvdReader

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_path doesn’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:

TelemedTvdReader

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]. Raises KeyError if 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_0n in ms, with frame 0 -> 0.0.

frame_idx_0n is 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:

TelemedRecordingMeta

telemed._extract.connect()

Build + connect a TelemedTvdReader in one call.

Return type:

TelemedTvdReader

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 .tvd recording(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 composite process() does both in one call.

source may be:

  • A path to a single .tvd file.

  • A directory (walked for pattern files; recursive=True by default).

  • An iterable of any combination of the above.

For each .tvd, a sibling <stem>.tvd.h5 is written. With skip_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 OpenFile fails on UNC / mapped-network paths. When copy_to_local is True (or None – 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 of local_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.h5 sidecar 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; None skips 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. None uses the system temp directory.

  • keep_full_speed (bool) – If True (default), opt the Python process and the running EchoWave.exe out 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; see telemed.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 with success=True when the extract wrote the local .h5, False when 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 the dustrack.batch convention.

  • 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), or f"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=False with an explicit crf= 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/gray and 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_changed is 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-zero b_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.h5 recording(s) to .mp4 file(s).

Single unified entry point. source may be:

  • A path to a single .tvd.h5 file (encoded as-is).

  • A path to a .tvd file (its sibling .tvd.h5 is 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_images outputs are written: single-probe -> <stem>.mp4, multi-probe (n>=2) -> <stem>_b{img_id}.mp4 per 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 pattern walk.

  • 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 uses crf. 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 -preset value. Default "ultrafast" (lossless bit-exact regardless of preset; ultrafast trades ~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’s mean_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=True so 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 .mp4 already exists. (Each panel checked independently for multi-probe sidecars.)

  • overwrite (bool) – When True, clobber any existing target .mp4. Mutually exclusive with skip_existing=True; if both, overwrite wins.

  • build_toc (bool) – When True (default), build the <mp4>.dnav-toc sidecar after each successful encode (and rebuild a missing sidecar on the skip-existing path) so dnav.VideoReader / DUSTrack open the mp4 without the cold-open demux pass. Silently skipped (with a one- time warning) when datanavigator isn’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 desc is the output mp4 stem). False suppresses both.

  • progress_callback (Callable[[int, int, Path, str], None] | None) – Optional fn(idx, total, mp4_path, status) – matches the dustrack.batch convention. idx / total are 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: .tvd files with no sibling .tvd.h5 (need extraction)

  • Set B: .tvd.h5 files 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 (.tvd without 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.h5 already 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.h5 skips extract; existing .mp4 skips encode; existing .dnav-toc skips TOC build. Force a full rebuild with skip_existing=False, overwrite=True.

Kwargs are signature-routed:

  • h5-only kwargs (frames, compression, compression_opts, copy_to_local, local_temp_root) -> Pipeline A’s export_h5.

  • video-only kwargs (out_dir, codec, lossless, crf, preset, fps, normalize_orientation, crop, build_toc, overwrite) -> export_video in 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_id follows the AutoInt1 convention (1=B, 2=B2, 3=B3, 4=B4); physical_d{x,y}_cm_per_px is 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.h5 sidecar.

Parameters:

fname (Union[str, os.PathLike]) – Path to the HDF5 sidecar (<stem>.tvd.h5) produced by extract_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 own physical_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 .tvd container header, stored at extract time. None on sidecars extracted before the completeness-QC feature. When this sits well above n_frames, EchoWave truncated the load to fit memory – audit a cohort with telemed.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 integers 1..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 acquisition b_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 / orientation b_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, sanity is_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 b_mode_roi: Roi

Primary B-mode ROI (img_id=1). Alias for b_mode_rois[1].

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, prefer image_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, use image_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_px per 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. Returns None when the sidecar has no b_depth param either (v1a1 schemas).

Use this for cm conversions on tracked-point coordinates – physical_dy{N}_cm_per_px is 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_dy for all probed acquisitions. So the display x scale equals image_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 in view(); revisit then.

Stored as a root attr in v1a5+; back-compat fallback via image_dy_cm_per_px for 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. True or "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_id of the panel to crop to (1=B, 2=B2, …). Defaults to 1 so single-probe call sites work unchanged. Validated even when crop=False so a typo doesn’t silently pass.

Returns:

np.ndarray of shape (H, W) – full frame or cropped depending on crop.

Raises:
  • RuntimeError – If the HDF5 was written without frames (extract_recording(..., frames=False)).

  • IndexError – If frame_idx_0n is out of range.

  • KeyError – If panel is not an active img_id.

  • ValueError – If crop is 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 chosen out_dir. Mirrors export_video’s naming so downstream callers don’t recreate the convention:

  • single-probe -> <stem>.mp4

  • multi-probe -> <stem>_b{panel}.mp4

Does NOT check whether the file exists – pair with ensure_mp4() to encode if missing.

Parameters:
  • panel (int) – img_id of the panel (1=B, 2=B2, …). Must be an active panel in b_mode_rois.

  • out_dir (str | PathLike | None) – Output directory. None (default) co-locates next to self.fname – matches to_video()’s default.

Raises:

KeyErrorpanel is 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_id of the panel to return.

  • out_dir (str | PathLike | None) – Output directory. None (default) co-locates with self.fname. Forwarded to both the existence check and the encode pass so they agree on where to look.

  • **encode_kwargs – Forwarded to export_videolossless / crf / preset / fps / normalize_orientation / overwrite etc. See export_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}.mp4 per 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 to self.fname.

  • **kwargs – Forwarded to export_videolossless, crf, preset, fps, normalize_orientation, overwrite, etc. See export_video docstring.

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 Figure so the caller can keep a reference (or call plt.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_nvenc uses VBR with constant-quality; libx264 uses 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 dst exists.

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). With mono=False, encoder=None you 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 internal encoder_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 in S:/_corpus/telemed/_bench/). Lower values (e.g. 22) buy tighter parity at larger file size; higher (e.g. 26 or 28) save more space. When encoder is not None (mono=False branch), defaults to 28 (consistent with video.py).

  • preset – ffmpeg -preset value. 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-up dustrack.batch.convert_to_mono() step. Drops audio along with chroma. Pass mono=False to restore the pre-graduation libx264 yuv420p path.

Deprecated since version 0.1.0: Use telemed.export_video() or telemed.process() against .tvd recordings 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*.mp4 under data_dir into dest_dir.

File discovery uses pyfilemanager.FileManager with include="telemed" and exclude="archive" — preserved from the pre-graduation pia02 / chi01 implementations so existing on-disk layouts continue to match.

Output filenames are <stem_core><left_suffix>.mp4 and <stem_core><right_suffix>.mp4 where stem_core is the source stem split on stem_split (default: first whitespace-delimited token, which strips the device-emitted trailing description + timestamp).

Default is mono=True (h265 4:0:0 monochrome). Pass mono=False to fall back to the pre-graduation libx264 yuv420p path; see crop_video() for the trade-offs.

Deprecated since version 0.1.0: Use telemed.export_video() or telemed.process() against .tvd recordings 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.txt warns (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 specific ParamGet* 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_path in 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_* NUMBER and classify how to probe it.

Strategies:

  • documented_get – description text contains an explicit ParamGet{Int|Bool|Double|Float|String}(...) reference. Probed with that variant. (Float is normalised to Double: both return numeric and Double is higher precision.)

  • shift_inferred – name contains _shift and 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 as shift_inferred so “doc says so” stays distinguishable from “convention says so”.

  • action_only – description contains val = 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 as params plus an err field 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; constant should 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 result as 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