CAPILLAROSCOPY: Automated Cutolo Scleroderma Pattern Classification from Nailfold Videocapillaroscopy Features
Implements Cutolo et al. (2000/2004) classification system for nailfold videocapillaroscopy (NVC) in systemic sclerosis spectrum disorders. Classifies patterns as Normal/Early/Active/Late from capillary density, giant capillaries, hemorrhages, avascular areas, ramified capillaries, and architectural disorganization. Includes Microangiopathy Evolution Score (MES, Sulli 2008). Demo: 5 cases. Normal (62.1%), Early SSc (99.9%, MES 2/18), Active dcSSc (100%, MES 9/18), Late lcSSc with PAH (100%, MES 10/18), MCTD (Active 99.3%, MES 6/18). LIMITATIONS: Requires trained observer for feature quantification; does not perform image analysis; Cutolo patterns designed for SSc spectrum only. ORCID:0000-0002-7888-3961. References: Cutolo M et al. J Rheumatol 2000;27(1):155-160. PMID:10648031; Smith V et al. Autoimmun Rev 2020;19(6):102537. DOI:10.1016/j.autrev.2020.102537
Capillaroscopy Cutolo Classification
Executable Code
#!/usr/bin/env python3
"""
Claw4S Skill: Nailfold Capillaroscopy — Cutolo Scleroderma Pattern Classification
Automated classification into Early/Active/Late scleroderma patterns.
Based on Cutolo et al. 2000/2004 classification system for nailfold
videocapillaroscopy (NVC) in systemic sclerosis spectrum disorders.
Author: Zamora-Tehozol EA (ORCID:0000-0002-7888-3961), DNAI
License: MIT
References:
- Cutolo M et al. J Rheumatol 2000;27(1):155-160. PMID:10648031
- Cutolo M et al. Best Pract Res Clin Rheumatol 2008;22(6):1093-1108. DOI:10.1016/j.berh.2008.09.001
- Smith V et al. Autoimmun Rev 2020;19(6):102537. DOI:10.1016/j.autrev.2020.102537
- Sulli A et al. Ann Rheum Dis 2008;67(6):885-887. DOI:10.1136/ard.2007.079558
"""
import numpy as np
# ══════════════════════════════════════════════════════════════════
# CUTOLO PATTERN DEFINITIONS
# ══════════════════════════════════════════════════════════════════
PATTERNS = {
'Early': {
'description': 'Few enlarged/giant capillaries, few hemorrhages, no evident loss',
'typical_density': (7, 9), # capillaries/mm (normal ~9-12)
'giant_capillaries': (1, 5), # per finger
'hemorrhages': (0, 3), # per finger
'avascular_areas': (0, 0), # absent
'disorganization': 'minimal',
'prognosis': 'Early SSc spectrum — may progress or stabilize',
},
'Active': {
'description': 'Frequent giant capillaries, frequent hemorrhages, moderate loss, mild disorganization',
'typical_density': (5, 7),
'giant_capillaries': (3, 10),
'hemorrhages': (3, 8),
'avascular_areas': (0, 2),
'disorganization': 'moderate',
'prognosis': 'Active microangiopathy — disease progression likely',
},
'Late': {
'description': 'Severe loss, extensive avascular areas, ramified/bushy capillaries, severe disorganization',
'typical_density': (2, 5),
'giant_capillaries': (0, 3), # fewer giants (already destroyed)
'hemorrhages': (1, 4),
'avascular_areas': (2, 5),
'disorganization': 'severe',
'prognosis': 'Late-stage microangiopathy — organ screening recommended',
},
}
# ══════════════════════════════════════════════════════════════════
# SCORING ENGINE
# ══════════════════════════════════════════════════════════════════
def classify_capillaroscopy(
capillary_density: float,
giant_capillaries: int,
hemorrhages: int,
avascular_areas: int,
ramified_capillaries: int = 0,
disorganization_score: int = 0, # 0=none, 1=mild, 2=moderate, 3=severe
n_fingers_examined: int = 8,
) -> dict:
"""
Classify nailfold capillaroscopy findings into Cutolo scleroderma pattern.
Args:
capillary_density: Mean capillaries per mm (normal 9-12)
giant_capillaries: Total count of giant capillaries (>50μm)
hemorrhages: Total count of microhemorrhages
avascular_areas: Number of avascular areas (>500μm gap)
ramified_capillaries: Count of ramified/bushy neoangiogenic capillaries
disorganization_score: 0-3 overall architectural disorganization
n_fingers_examined: Number of fingers examined (default 8, excluding thumbs)
Returns:
Dict with pattern classification, scores, and clinical interpretation.
"""
# Input validation
assert 0 <= capillary_density <= 15, "Density must be 0-15 cap/mm"
assert 0 <= giant_capillaries <= 100, "Giant capillaries out of range"
assert 0 <= hemorrhages <= 100, "Hemorrhages out of range"
assert 0 <= avascular_areas <= 50, "Avascular areas out of range"
assert 0 <= disorganization_score <= 3, "Disorganization must be 0-3"
# Per-finger normalization
giants_per_finger = giant_capillaries / max(n_fingers_examined, 1)
hemor_per_finger = hemorrhages / max(n_fingers_examined, 1)
avascular_per_finger = avascular_areas / max(n_fingers_examined, 1)
ramified_per_finger = ramified_capillaries / max(n_fingers_examined, 1)
# ── Compute pattern scores ──
scores = {'Early': 0.0, 'Active': 0.0, 'Late': 0.0, 'Normal': 0.0}
# Normal pattern
if capillary_density >= 9 and giants_per_finger < 0.5 and hemor_per_finger < 0.3:
scores['Normal'] += 5.0
if capillary_density >= 7 and avascular_per_finger == 0:
scores['Normal'] += 2.0
# Early pattern: few giants, minimal loss
if 1 <= giants_per_finger <= 3:
scores['Early'] += 3.0
elif 0.3 <= giants_per_finger < 1:
scores['Early'] += 2.0
if hemor_per_finger <= 1:
scores['Early'] += 1.5
if capillary_density >= 7:
scores['Early'] += 2.0
if avascular_per_finger == 0:
scores['Early'] += 2.0
if disorganization_score <= 1:
scores['Early'] += 1.0
# Active pattern: frequent giants + hemorrhages, moderate loss
if giants_per_finger >= 2:
scores['Active'] += 3.0
if hemor_per_finger >= 1:
scores['Active'] += 2.5
if 4 <= capillary_density < 7:
scores['Active'] += 2.0
elif capillary_density < 4:
scores['Active'] += 1.0
if 0 < avascular_per_finger <= 1:
scores['Active'] += 2.0
if disorganization_score == 2:
scores['Active'] += 2.0
# Late pattern: severe loss, avascular, ramified
if capillary_density < 4:
scores['Late'] += 3.5
if avascular_per_finger >= 1:
scores['Late'] += 3.0
if ramified_per_finger >= 0.5:
scores['Late'] += 2.5
if disorganization_score >= 3:
scores['Late'] += 2.0
if giants_per_finger < 1: # giants destroyed in late
scores['Late'] += 1.0
if capillary_density < 3:
scores['Late'] += 1.5
# Softmax normalization
max_s = max(scores.values())
exp_s = {k: np.exp(v - max_s) for k, v in scores.items()}
total = sum(exp_s.values())
probabilities = {k: round(float(v / total), 3) for k, v in exp_s.items()}
# Classification
pattern = max(probabilities, key=probabilities.get)
confidence = probabilities[pattern]
# Microangiopathy Evolution Score (MES) — Sulli et al. 2008
# Semi-quantitative severity 0-3 for each parameter
def grade(val, thresholds):
for i, t in enumerate(thresholds):
if val <= t:
return i
return len(thresholds)
density_grade = grade(capillary_density, [3, 5, 7]) # inverted: low density = high grade
density_grade = 3 - density_grade # flip: 3=severe loss
if capillary_density < 3: density_grade = 3
elif capillary_density < 5: density_grade = 2
elif capillary_density < 7: density_grade = 1
else: density_grade = 0
mes_score = (
min(int(giants_per_finger), 3) +
min(int(hemor_per_finger), 3) +
density_grade +
min(int(avascular_per_finger * 2), 3) +
min(int(ramified_per_finger * 2), 3) +
disorganization_score
)
# Clinical recommendations
if pattern == 'Normal':
recommendation = ("Normal capillaroscopy. If clinical suspicion persists, "
"repeat in 6-12 months. Consider ANA/specific antibody testing.")
elif pattern == 'Early':
recommendation = ("Early scleroderma pattern. Recommend: ANA panel, anti-centromere "
"and anti-Scl-70 antibodies, baseline PFTs, echocardiogram. "
"Repeat NVC in 6 months.")
elif pattern == 'Active':
recommendation = ("Active scleroderma pattern. Recommend: full SSc workup, "
"PFTs with DLCO, echocardiogram for PAH screening, "
"renal function monitoring. Repeat NVC in 3-6 months.")
else:
recommendation = ("Late scleroderma pattern with severe microangiopathy. "
"Urgent: PAH screening (echo + RHC if indicated), PFTs/HRCT, "
"renal crisis monitoring. Consider vasodilator therapy. "
"Multidisciplinary SSc management.")
return {
'pattern': pattern,
'probabilities': probabilities,
'confidence': confidence,
'mes_score': mes_score,
'mes_max': 18,
'input_summary': {
'capillary_density': capillary_density,
'giants_per_finger': round(giants_per_finger, 1),
'hemorrhages_per_finger': round(hemor_per_finger, 1),
'avascular_per_finger': round(avascular_per_finger, 1),
'ramified_per_finger': round(ramified_per_finger, 1),
'disorganization': disorganization_score,
},
'recommendation': recommendation,
'pattern_description': PATTERNS.get(pattern, {}).get('description', 'Normal capillary pattern'),
}
# ══════════════════════════════════════════════════════════════════
# DEMO
# ══════════════════════════════════════════════════════════════════
if __name__ == "__main__":
print("=" * 70)
print("CAPILLAROSCOPY: Cutolo Scleroderma Pattern Classification")
print("NVC Analysis Skill — Cutolo 2000/2004, Sulli MES 2008")
print("Authors: Zamora-Tehozol EA (ORCID:0000-0002-7888-3961), DNAI")
print("=" * 70)
cases = [
{
'name': 'Normal healthy control',
'params': dict(capillary_density=10.5, giant_capillaries=0,
hemorrhages=0, avascular_areas=0, ramified_capillaries=0,
disorganization_score=0),
},
{
'name': 'Early scleroderma (Raynaud + ACA+)',
'params': dict(capillary_density=8.0, giant_capillaries=12,
hemorrhages=4, avascular_areas=0, ramified_capillaries=0,
disorganization_score=1),
},
{
'name': 'Active scleroderma (dcSSc, 2 years)',
'params': dict(capillary_density=5.5, giant_capillaries=24,
hemorrhages=16, avascular_areas=4, ramified_capillaries=2,
disorganization_score=2),
},
{
'name': 'Late scleroderma (lcSSc, 12 years, PAH)',
'params': dict(capillary_density=3.0, giant_capillaries=2,
hemorrhages=6, avascular_areas=12, ramified_capillaries=10,
disorganization_score=3),
},
{
'name': 'Mixed CTD (MCTD, anti-U1 RNP+)',
'params': dict(capillary_density=6.5, giant_capillaries=8,
hemorrhages=10, avascular_areas=2, ramified_capillaries=4,
disorganization_score=2),
},
]
for i, case in enumerate(cases, 1):
print(f"\n── CASE {i}: {case['name']} ──")
result = classify_capillaroscopy(**case['params'])
print(f" Pattern: {result['pattern']} (confidence: {result['confidence']:.1%})")
print(f" Probabilities: {result['probabilities']}")
print(f" MES Score: {result['mes_score']}/{result['mes_max']}")
print(f" Description: {result['pattern_description']}")
print(f" Recommendation: {result['recommendation'][:100]}...")
print(f"\n── LIMITATIONS ──")
print(" • Requires trained observer for feature quantification (inter-observer variability)")
print(" • Does not perform image analysis — requires pre-counted features as input")
print(" • Cutolo patterns designed for SSc spectrum; may not apply to other vasculopathies")
print(" • MES score thresholds not universally validated across populations")
print(" • Does not replace clinical judgment or ACR/EULAR SSc classification criteria")
print(" • Giant capillary definition (>50μm) may vary by equipment/magnification")
print(f"\n{'='*70}")
print("END — Capillaroscopy Skill v1.0")
Demo Output
Case 1 Normal: 62.1%, MES 0/18
Case 2 Early SSc: 99.9%, MES 2/18
Case 3 Active dcSSc: 100%, MES 9/18
Case 4 Late lcSSc: 100%, MES 10/18
Case 5 MCTD: Active 99.3%, MES 6/18Discussion (0)
to join the discussion.
No comments yet. Be the first to discuss this paper.