DIGITS™ brings computer-vision assessments to your research pipeline.
Validated against certified current gold standard.
No hardware. Just structured data.
IN COLLABORATION WITH
Published Research
DIGITS has been evaluated in studies published in the Journal of Hand Surgery and the Journal of Hand Therapy — spanning range of motion, finger dexterity, and hand volume.
The goniometer has been the gold standard for hand ROM measurement since the 1970s — it is cheap, familiar, and entirely dependent on who is holding it. We set out to ask whether computer vision could match clinical-grade repeatability without the clinician. The answer is that it can — and from a patient's own phone.
Abstract
Concurrent Validity of Augmented Reality-Based Finger Joint Range of Motion Measurement Versus Manual Goniometry in Pathologic Hands
Purpose: Finger range of motion (ROM) measurement is essential for tracking performance in the setting of hand injury or pathology. Augmented-reality (AR) tracking is developing as an alternative to manual goniometry. This study aimed to evaluate the concurrent validity of AR measurements compared against goniometry in a pathologic hand population.
Methods: Augmented-reality landmark tracking using the Google MediaPipe Hands framework calculates finger joint ROM angles in real time. Participants performed a series of hand poses captured from three camera orientations: palmar, radial oblique, and ulnar oblique. Flexion and extension ROM was measured at the metacarpophalangeal (MCP), proximal interphalangeal (PIP), and distal interphalangeal (DIP) joints of the pathologic index through small fingers. AR measurements were compared to manual goniometry, with concurrent validity categorized by median difference <10° (great), 10–20° (moderate), and >20° (poor).
Results: Thirty-two hands with pathology were included in the analysis. The median age was 59.0 years (interquartile range: 16.0–72.3) and 62.5% of participants were male. The most common hand pathologies were degenerative (46.9%) and trauma (43.8%). For flexion measurements, 50.0% of median differences in joint ROM were within 10° and 41.7% were between 10–20° of the goniometer. For extension, 66.7% of median differences in joint ROM were within 10° and 33.3% were between 10–20°. Overall, 95.8% of all ROM measurements fell within 20° of median difference. Radial oblique camera views provided the most accurate measurements for 45.8% of joints, followed by ulnar oblique (37.5%) and palmar (16.7%).
Conclusion: Augmented reality tracking demonstrates high concurrent validity for extension ROM measurements, with joint-dependent variability. Flexion measurements were less consistent due to obstructed camera views of the fingers. Further optimization and training using pathologic-hand data is required prior to widespread adoption of ROM measurements using augmented reality.
Developer Experience
A RESTful API built for research workflows. Structured JSON, FHIR R4 export, and SDKs for Python and R.
Landmark detection delivers joint angles across all fingers with excellent test-retest reliability (ICC >0.75), validated against a the current gold standard. Ongoing validation in clinical populations is in progress.
Already collecting hand data? Submit timestamped captures as CSV or JSON and receive standardised joint-angle scores and normative comparisons in return — no DIGITS hardware or app required. Query the growing normative database to benchmark cohorts by age, sex, and injury type, or spot population-level trends across your study.
Data is encrypted in transit and at rest, with role-based access controls, audit logging, and de-identification at capture. Our data-usage, storage, and retention practices are built to meet the privacy and security standards research institutions require, including HIPAA and PIPEDA.
Participants rate the experience highly, and both patients and researchers can attach validated questionnaires and instruments (such as PROMs) to any capture — pairing objective joint-angle data with standardised patient-reported outcomes.
# DIGITS Research Connect — API Quickstart import digits_rc from datetime import datetime, timedelta client = digits_rc.Client( api_key="drc_live_••••••••••••" ) # ── List recent completed assessments ─────── page = client.assessments.list( status="completed", since=datetime.now() - timedelta(days=7) ) print(f"Found {page.total} assessments") # ── Retrieve joint-angle data ──────────────── result = client.assessments.retrieve( id="asmt_Kp9mXjQ2vR" ) for joint in result.joints: print(joint.name, joint.flexion_deg, joint.normative_deg) # MCP_INDEX 85.2° (norm 90°) ✓ # PIP_INDEX 89.7° (norm 100°) ✓ # DIP_INDEX 70.1° (norm 80°) ✓ # MCP_MIDDLE 88.0° (norm 90°) ✓ # PIP_MIDDLE 91.4° (norm 100°) ✓ # THUMB_CMC 44.8° (norm 50°) ✓ # ── Normative z-score comparison ──────────── norm = client.norms.compare( subject_id="subj_Wp3nKq8mL", age=42, sex="M" ) for row in norm.deviations: flag = "⚠" if row.z_score < -1.5 else "✓" print(f"{row.joint:16} z={row.z_score:+.2f} {flag}") # ── FHIR R4 export ─────────────────────────── fhir = result.to_fhir() client.fhir.push(fhir, endpoint=ehr_url) # ── Register webhook ───────────────────────── hook = client.webhooks.create( url="https://api.clinic.io/drc/events", events=["assessment.completed"], secret="whs_••••••••••••" ) print(f"Webhook active: {hook.id}") # ── Batch subject import ───────────────────── subjects = client.subjects.bulk_create([ {"external_id": "PT-001", "dob": "1982-03-14"}, {"external_id": "PT-002", "dob": "1965-08-29"}, {"external_id": "PT-003", "dob": "1991-11-07"}, ]) print(f"Imported {len(subjects)} subjects") # ── ICC validation report ──────────────────── report = client.validation.icc_report( site_id="site_MAYO_01", joint="PIP_INDEX", n_raters=3 ) print(f"ICC(3,1) = {report.icc:.3f} " f"CI [{report.ci_lower:.3f}, {report.ci_upper:.3f}]") # ── Longitudinal cohort aggregate ──────────── agg = client.reports.aggregate( trial_id="trial_FRACT_2026_B", metric="pip_index_flexion", group_by="week" ) for site in agg.sites: print(f"{site.name:20} n={site.n:4} " f"mean={site.mean:.1f}°")
# DIGITS Research Connect — API Quickstart import digits_rc from datetime import datetime, timedelta client = digits_rc.Client( api_key="drc_live_••••••••••••" ) # ── List recent completed assessments ─────── page = client.assessments.list( status="completed", since=datetime.now() - timedelta(days=7) ) print(f"Found {page.total} assessments") # ── Retrieve joint-angle data ──────────────── result = client.assessments.retrieve( id="asmt_Kp9mXjQ2vR" ) for joint in result.joints: print(joint.name, joint.flexion_deg, joint.normative_deg) # MCP_INDEX 85.2° (norm 90°) ✓ # PIP_INDEX 89.7° (norm 100°) ✓ # DIP_INDEX 70.1° (norm 80°) ✓ # MCP_MIDDLE 88.0° (norm 90°) ✓ # PIP_MIDDLE 91.4° (norm 100°) ✓ # THUMB_CMC 44.8° (norm 50°) ✓ # ── Normative z-score comparison ──────────── norm = client.norms.compare( subject_id="subj_Wp3nKq8mL", age=42, sex="M" ) for row in norm.deviations: flag = "⚠" if row.z_score < -1.5 else "✓" print(f"{row.joint:16} z={row.z_score:+.2f} {flag}") # ── FHIR R4 export ─────────────────────────── fhir = result.to_fhir() client.fhir.push(fhir, endpoint=ehr_url) # ── Register webhook ───────────────────────── hook = client.webhooks.create( url="https://api.clinic.io/drc/events", events=["assessment.completed"], secret="whs_••••••••••••" ) print(f"Webhook active: {hook.id}") # ── Batch subject import ───────────────────── subjects = client.subjects.bulk_create([ {"external_id": "PT-001", "dob": "1982-03-14"}, {"external_id": "PT-002", "dob": "1965-08-29"}, {"external_id": "PT-003", "dob": "1991-11-07"}, ]) print(f"Imported {len(subjects)} subjects") # ── ICC validation report ──────────────────── report = client.validation.icc_report( site_id="site_MAYO_01", joint="PIP_INDEX", n_raters=3 ) print(f"ICC(3,1) = {report.icc:.3f} " f"CI [{report.ci_lower:.3f}, {report.ci_upper:.3f}]") # ── Longitudinal cohort aggregate ──────────── agg = client.reports.aggregate( trial_id="trial_FRACT_2026_B", metric="pip_index_flexion", group_by="week" ) for site in agg.sites: print(f"{site.name:20} n={site.n:4} " f"mean={site.mean:.1f}°")
# DIGITS Research Connect — API Quickstart import digits_rc from datetime import datetime, timedelta client = digits_rc.Client( api_key="drc_live_••••••••••••" ) # ── List recent completed assessments ─────── page = client.assessments.list( status="completed", since=datetime.now() - timedelta(days=7) ) print(f"Found {page.total} assessments") # ── Retrieve joint-angle data ──────────────── result = client.assessments.retrieve( id="asmt_Kp9mXjQ2vR" ) for joint in result.joints: print(joint.name, joint.flexion_deg, joint.normative_deg) # MCP_INDEX 85.2° (norm 90°) ✓ # PIP_INDEX 89.7° (norm 100°) ✓ # DIP_INDEX 70.1° (norm 80°) ✓ # MCP_MIDDLE 88.0° (norm 90°) ✓ # PIP_MIDDLE 91.4° (norm 100°) ✓ # THUMB_CMC 44.8° (norm 50°) ✓ # ── Normative z-score comparison ──────────── norm = client.norms.compare( subject_id="subj_Wp3nKq8mL", age=42, sex="M" ) for row in norm.deviations: flag = "⚠" if row.z_score < -1.5 else "✓" print(f"{row.joint:16} z={row.z_score:+.2f} {flag}") # ── FHIR R4 export ─────────────────────────── fhir = result.to_fhir() client.fhir.push(fhir, endpoint=ehr_url) # ── Register webhook ───────────────────────── hook = client.webhooks.create( url="https://api.clinic.io/drc/events", events=["assessment.completed"], secret="whs_••••••••••••" ) print(f"Webhook active: {hook.id}") # ── Batch subject import ───────────────────── subjects = client.subjects.bulk_create([ {"external_id": "PT-001", "dob": "1982-03-14"}, {"external_id": "PT-002", "dob": "1965-08-29"}, {"external_id": "PT-003", "dob": "1991-11-07"}, ]) print(f"Imported {len(subjects)} subjects") # ── ICC validation report ──────────────────── report = client.validation.icc_report( site_id="site_MAYO_01", joint="PIP_INDEX", n_raters=3 ) print(f"ICC(3,1) = {report.icc:.3f} " f"CI [{report.ci_lower:.3f}, {report.ci_upper:.3f}]") # ── Longitudinal cohort aggregate ──────────── agg = client.reports.aggregate( trial_id="trial_FRACT_2026_B", metric="pip_index_flexion", group_by="week" ) for site in agg.sites: print(f"{site.name:20} n={site.n:4} " f"mean={site.mean:.1f}°")
# DIGITS Research Connect — API Quickstart import digits_rc from datetime import datetime, timedelta client = digits_rc.Client( api_key="drc_live_••••••••••••" ) # ── List recent completed assessments ─────── page = client.assessments.list( status="completed", since=datetime.now() - timedelta(days=7) ) print(f"Found {page.total} assessments") # ── Retrieve joint-angle data ──────────────── result = client.assessments.retrieve( id="asmt_Kp9mXjQ2vR" ) for joint in result.joints: print(joint.name, joint.flexion_deg, joint.normative_deg) # MCP_INDEX 85.2° (norm 90°) ✓ # PIP_INDEX 89.7° (norm 100°) ✓ # DIP_INDEX 70.1° (norm 80°) ✓ # MCP_MIDDLE 88.0° (norm 90°) ✓ # PIP_MIDDLE 91.4° (norm 100°) ✓ # THUMB_CMC 44.8° (norm 50°) ✓ # ── Normative z-score comparison ──────────── norm = client.norms.compare( subject_id="subj_Wp3nKq8mL", age=42, sex="M" ) for row in norm.deviations: flag = "⚠" if row.z_score < -1.5 else "✓" print(f"{row.joint:16} z={row.z_score:+.2f} {flag}") # ── FHIR R4 export ─────────────────────────── fhir = result.to_fhir() client.fhir.push(fhir, endpoint=ehr_url) # ── Register webhook ───────────────────────── hook = client.webhooks.create( url="https://api.clinic.io/drc/events", events=["assessment.completed"], secret="whs_••••••••••••" ) print(f"Webhook active: {hook.id}") # ── Batch subject import ───────────────────── subjects = client.subjects.bulk_create([ {"external_id": "PT-001", "dob": "1982-03-14"}, {"external_id": "PT-002", "dob": "1965-08-29"}, {"external_id": "PT-003", "dob": "1991-11-07"}, ]) print(f"Imported {len(subjects)} subjects") # ── ICC validation report ──────────────────── report = client.validation.icc_report( site_id="site_MAYO_01", joint="PIP_INDEX", n_raters=3 ) print(f"ICC(3,1) = {report.icc:.3f} " f"CI [{report.ci_lower:.3f}, {report.ci_upper:.3f}]") # ── Longitudinal cohort aggregate ──────────── agg = client.reports.aggregate( trial_id="trial_FRACT_2026_B", metric="pip_index_flexion", group_by="week" ) for site in agg.sites: print(f"{site.name:20} n={site.n:4} " f"mean={site.mean:.1f}°")