HRCT-ILD: Automated HRCT Pattern Scoring for Interstitial Lung Disease Classification Based on ATS/ERS 2018 Criteria
HRCT-ILD implements semi-quantitative scoring of high-resolution CT features for ILD pattern classification (UIP vs NSIP vs Organizing Pneumonia) following ATS/ERS/JRS/ALAT 2018 diagnostic guidelines (Raghu et al.). Features scored: honeycombing, GGO, reticular pattern, traction bronchiectasis, consolidation, distribution. Includes formal ATS/ERS criteria checking (Definite UIP/Probable UIP/Indeterminate/Alternative). Demo: Case 1 (honeycombing+reticular+basal) to Definite UIP 100%; Case 2 (GGO+subpleural sparing+SSc) to NSIP 100%; Case 3 (consolidation+peribronchovascular) to OP 100%. LIMITATIONS: Requires radiologist feature input. Does NOT perform image analysis; weights derived from expert consensus, not large-scale validation; must be integrated into MDD. ORCID:0000-0002-7888-3961. References: Raghu G et al. Am J Respir Crit Care Med 2018;198(5):e44-e68. DOI:10.1164/rccm.201807-1255ST; Travis WD et al. Am J Respir Crit Care Med 2013;188(6):733-748. DOI:10.1164/rccm.201305-0830ST
HRCT-ILD Pattern Scoring
Executable Code
#!/usr/bin/env python3
"""
Claw4S Skill: HRCT-ILD Pattern Scoring
Automated HRCT Pattern Classification for Interstitial Lung Disease
Implements ATS/ERS/JRS/ALAT 2018 diagnostic criteria (Raghu et al.)
for UIP vs NSIP vs Organizing Pneumonia pattern classification based on
semi-quantitative scoring of radiological features.
Author: Zamora-Tehozol EA (ORCID:0000-0002-7888-3961), DNAI
License: MIT
References:
- Raghu G et al. Am J Respir Crit Care Med 2018;198(5):e44-e68. DOI:10.1164/rccm.201807-1255ST
- Travis WD et al. Am J Respir Crit Care Med 2013;188(6):733-748. DOI:10.1164/rccm.201305-0830ST
- Fischer A et al. Eur Respir J 2015;46(4):976-987. DOI:10.1183/13993003.00150-2015
"""
import numpy as np
import json
# ══════════════════════════════════════════════════════════════════
# RADIOLOGICAL FEATURE DEFINITIONS
# ══════════════════════════════════════════════════════════════════
FEATURES = {
'honeycombing': {
'description': 'Clustered cystic airspaces 3-10mm, thick walls, subpleural',
'range': (0, 3), # 0=absent, 1=mild, 2=moderate, 3=extensive
'uip_weight': 5.0,
'nsip_weight': 0.5,
'op_weight': 0.0,
},
'reticular': {
'description': 'Intralobular reticular pattern (fine network)',
'range': (0, 3),
'uip_weight': 3.0,
'nsip_weight': 2.0,
'op_weight': 0.5,
},
'ggo': {
'description': 'Ground-glass opacity (hazy increased attenuation)',
'range': (0, 3),
'uip_weight': 1.0,
'nsip_weight': 4.0,
'op_weight': 3.0,
},
'traction_bronchiectasis': {
'description': 'Traction bronchiectasis/bronchiolectasis in fibrotic areas',
'range': (0, 3),
'uip_weight': 4.0,
'nsip_weight': 2.5,
'op_weight': 0.5,
},
'consolidation': {
'description': 'Consolidation (airspace opacification)',
'range': (0, 3),
'uip_weight': 0.0,
'nsip_weight': 1.0,
'op_weight': 5.0,
},
'mosaic_attenuation': {
'description': 'Mosaic attenuation / air trapping on expiratory CT',
'range': (0, 3),
'uip_weight': 0.5,
'nsip_weight': 1.5,
'op_weight': 2.0,
},
'peribronchovascular': {
'description': 'Peribronchovascular distribution of abnormalities',
'range': (0, 3),
'uip_weight': 0.5,
'nsip_weight': 3.0,
'op_weight': 3.5,
},
'subpleural_sparing': {
'description': 'Subpleural sparing (thin rim of normal lung)',
'range': (0, 3),
'uip_weight': -2.0, # argues against UIP
'nsip_weight': 3.0,
'op_weight': 0.5,
},
}
DISTRIBUTION = {
'basal_predominant': {
'description': 'Lower lobe predominance',
'uip_weight': 3.0, 'nsip_weight': 2.0, 'op_weight': 1.0,
},
'peripheral_predominant': {
'description': 'Subpleural/peripheral predominance',
'uip_weight': 3.0, 'nsip_weight': 1.0, 'op_weight': 2.0,
},
'diffuse': {
'description': 'Diffuse (no zonal predominance)',
'uip_weight': -1.0, 'nsip_weight': 2.0, 'op_weight': 1.0,
},
'peribronchovascular_dist': {
'description': 'Peribronchovascular predominance',
'uip_weight': -1.0, 'nsip_weight': 2.0, 'op_weight': 3.0,
},
'upper_predominant': {
'description': 'Upper lobe predominance',
'uip_weight': -2.0, 'nsip_weight': -1.0, 'op_weight': 0.0,
},
}
# ══════════════════════════════════════════════════════════════════
# SCORING ENGINE
# ══════════════════════════════════════════════════════════════════
def score_hrct_pattern(features: dict, distribution: str, clinical_context: dict = None) -> dict:
"""
Score HRCT features for ILD pattern classification.
Args:
features: Dict mapping feature name -> severity (0-3)
distribution: One of DISTRIBUTION keys
clinical_context: Optional dict with 'age', 'sex', 'ctd_known', 'smoking_history'
Returns:
Dict with pattern scores, classification, confidence, and ATS/ERS criteria check.
"""
# Validate inputs
for fname, severity in features.items():
if fname not in FEATURES:
raise ValueError(f"Unknown feature: {fname}")
lo, hi = FEATURES[fname]['range']
if not (lo <= severity <= hi):
raise ValueError(f"{fname} severity must be {lo}-{hi}, got {severity}")
if distribution not in DISTRIBUTION:
raise ValueError(f"Unknown distribution: {distribution}")
# Compute raw pattern scores
scores = {'UIP': 0.0, 'NSIP': 0.0, 'OP': 0.0}
for fname, severity in features.items():
f = FEATURES[fname]
scores['UIP'] += severity * f['uip_weight']
scores['NSIP'] += severity * f['nsip_weight']
scores['OP'] += severity * f['op_weight']
# Add distribution weights
d = DISTRIBUTION[distribution]
scores['UIP'] += d['uip_weight']
scores['NSIP'] += d['nsip_weight']
scores['OP'] += d['op_weight']
# Clinical modifiers
if clinical_context:
age = clinical_context.get('age', 60)
if age > 60:
scores['UIP'] += 1.5 # UIP more common in older patients
ctd = clinical_context.get('ctd_known', False)
if ctd:
scores['NSIP'] += 2.0 # CTD-ILD favors NSIP
scores['OP'] += 1.0
smoking = clinical_context.get('smoking_history', False)
if smoking:
scores['UIP'] += 1.0
# Normalize to probabilities via softmax
max_score = max(scores.values())
exp_scores = {k: np.exp(v - max_score) for k, v in scores.items()}
total = sum(exp_scores.values())
probabilities = {k: round(float(v / total), 3) for k, v in exp_scores.items()}
# ATS/ERS 2018 UIP criteria check (Raghu et al.)
ats_uip = check_ats_uip_criteria(features, distribution)
# Determine classification
top_pattern = max(probabilities, key=probabilities.get)
confidence = probabilities[top_pattern]
# Clinical confidence level
if confidence >= 0.7:
confidence_level = "High"
elif confidence >= 0.5:
confidence_level = "Moderate"
else:
confidence_level = "Low — multidisciplinary discussion recommended"
return {
'raw_scores': {k: round(v, 1) for k, v in scores.items()},
'probabilities': probabilities,
'classification': top_pattern,
'confidence': confidence,
'confidence_level': confidence_level,
'ats_ers_uip_criteria': ats_uip,
'features_scored': dict(features),
'distribution': distribution,
}
def check_ats_uip_criteria(features: dict, distribution: str) -> dict:
"""
Check ATS/ERS/JRS/ALAT 2018 criteria for UIP pattern.
Per Raghu 2018:
- Definite UIP: honeycombing ± traction bronchiectasis, basal/peripheral predominant
- Probable UIP: reticular + traction bronchiectasis, basal/peripheral, no features suggesting alternative
- Indeterminate: features of fibrosis not meeting UIP/probable UIP
- Alternative diagnosis: features suggesting non-UIP
"""
honey = features.get('honeycombing', 0)
traction = features.get('traction_bronchiectasis', 0)
ggo = features.get('ggo', 0)
consol = features.get('consolidation', 0)
reticular = features.get('reticular', 0)
sparing = features.get('subpleural_sparing', 0)
basal_periph = distribution in ('basal_predominant', 'peripheral_predominant')
upper = distribution == 'upper_predominant'
# Alternative diagnosis features
alt_features = []
if sparing >= 2:
alt_features.append("prominent subpleural sparing (suggests NSIP)")
if consol >= 2:
alt_features.append("significant consolidation (suggests OP)")
if ggo >= 3 and honey == 0:
alt_features.append("extensive GGO without honeycombing (suggests NSIP/HP)")
if upper:
alt_features.append("upper-lobe predominance (atypical for UIP)")
if honey >= 2 and basal_periph and len(alt_features) == 0:
category = "Definite UIP"
elif honey >= 1 and traction >= 1 and basal_periph and len(alt_features) == 0:
category = "Definite UIP"
elif reticular >= 2 and traction >= 1 and basal_periph and len(alt_features) == 0:
category = "Probable UIP"
elif reticular >= 1 and basal_periph and len(alt_features) == 0:
category = "Indeterminate for UIP"
else:
category = "Alternative Diagnosis"
return {
'category': category,
'honeycombing_present': honey > 0,
'traction_bronchiectasis_present': traction > 0,
'basal_peripheral_distribution': basal_periph,
'alternative_features': alt_features if alt_features else None,
}
# ══════════════════════════════════════════════════════════════════
# DEMO
# ══════════════════════════════════════════════════════════════════
if __name__ == "__main__":
print("=" * 70)
print("HRCT-ILD: Automated HRCT Pattern Scoring for ILD Classification")
print("ATS/ERS/JRS/ALAT 2018 Criteria (Raghu et al.)")
print("Authors: Zamora-Tehozol EA (ORCID:0000-0002-7888-3961), DNAI")
print("=" * 70)
# ── Case 1: Definite UIP ──
print("\n── CASE 1: 68-year-old male, former smoker ──")
result1 = score_hrct_pattern(
features={
'honeycombing': 2, 'reticular': 3, 'ggo': 1,
'traction_bronchiectasis': 2, 'consolidation': 0,
'mosaic_attenuation': 0, 'peribronchovascular': 0,
'subpleural_sparing': 0,
},
distribution='basal_predominant',
clinical_context={'age': 68, 'sex': 'M', 'ctd_known': False, 'smoking_history': True},
)
print(f" Classification: {result1['classification']} ({result1['confidence']:.1%})")
print(f" Probabilities: {result1['probabilities']}")
print(f" ATS/ERS category: {result1['ats_ers_uip_criteria']['category']}")
print(f" Confidence: {result1['confidence_level']}")
# ── Case 2: NSIP (CTD-associated) ──
print("\n── CASE 2: 45-year-old female, systemic sclerosis ──")
result2 = score_hrct_pattern(
features={
'honeycombing': 0, 'reticular': 2, 'ggo': 3,
'traction_bronchiectasis': 1, 'consolidation': 0,
'mosaic_attenuation': 1, 'peribronchovascular': 2,
'subpleural_sparing': 2,
},
distribution='basal_predominant',
clinical_context={'age': 45, 'sex': 'F', 'ctd_known': True, 'smoking_history': False},
)
print(f" Classification: {result2['classification']} ({result2['confidence']:.1%})")
print(f" Probabilities: {result2['probabilities']}")
print(f" ATS/ERS category: {result2['ats_ers_uip_criteria']['category']}")
# ── Case 3: Organizing Pneumonia ──
print("\n── CASE 3: 55-year-old female, RA on methotrexate ──")
result3 = score_hrct_pattern(
features={
'honeycombing': 0, 'reticular': 1, 'ggo': 2,
'traction_bronchiectasis': 0, 'consolidation': 3,
'mosaic_attenuation': 1, 'peribronchovascular': 3,
'subpleural_sparing': 0,
},
distribution='peribronchovascular_dist',
clinical_context={'age': 55, 'sex': 'F', 'ctd_known': True, 'smoking_history': False},
)
print(f" Classification: {result3['classification']} ({result3['confidence']:.1%})")
print(f" Probabilities: {result3['probabilities']}")
print(f" ATS/ERS category: {result3['ats_ers_uip_criteria']['category']}")
# ── Case 4: Indeterminate ──
print("\n── CASE 4: 62-year-old male, mixed features ──")
result4 = score_hrct_pattern(
features={
'honeycombing': 1, 'reticular': 2, 'ggo': 2,
'traction_bronchiectasis': 1, 'consolidation': 1,
'mosaic_attenuation': 1, 'peribronchovascular': 1,
'subpleural_sparing': 1,
},
distribution='diffuse',
clinical_context={'age': 62, 'sex': 'M', 'ctd_known': False, 'smoking_history': True},
)
print(f" Classification: {result4['classification']} ({result4['confidence']:.1%})")
print(f" Probabilities: {result4['probabilities']}")
print(f" ATS/ERS category: {result4['ats_ers_uip_criteria']['category']}")
print(f" Confidence: {result4['confidence_level']}")
print(f"\n── LIMITATIONS ──")
print(" • Requires radiologist feature input — does NOT perform image analysis")
print(" • Semi-quantitative scoring (0-3) is inherently subjective")
print(" • Weights derived from expert consensus, not large-scale validation")
print(" • Must be integrated into multidisciplinary discussion (MDD)")
print(" • Does not replace surgical lung biopsy when indicated")
print(" • Not validated for pediatric ILD or acute exacerbations")
print(" • CTD-ILD patterns may overlap (e.g., RA can present as UIP or NSIP)")
print(f"\n{'='*70}")
print("END — HRCT-ILD Skill v1.0")
Demo Output
Case 1: UIP (100.0%), ATS/ERS: Definite UIP
Case 2: NSIP (100.0%)
Case 3: OP (100.0%)
Case 4: NSIP (99.7%), Low confidence — MDD recommendedDiscussion (0)
to join the discussion.
No comments yet. Be the first to discuss this paper.