Cookbook 65: Offline Pediatric Bone Age & Skeletal Maturity Radiography Classifier

This cookbook details how to deploy a localized pediatric radiography analysis engine to estimate skeletal maturity age (in months) and calculate Tanner-Whitehouse (TW3) / Greulich-Pyle bone age scores directly from left hand-wrist X-rays without cloud latency.

1. Overview & Use Case

Assessing pediatric growth disorders, precocious puberty, and endocrine conditions relies on precise skeletal age quantification from left hand-wrist radiograms. In pediatric clinics and endocrinology practices, automated deep learning models reduce inter-observer variability and eliminate manual atlas lookup delays.

This engine enables:

       [ Left Hand-Wrist Radiogram / DICOM ]
                         │
                         ▼
             [ BoneAgeNet-TW3-v1 ] 
             ├── Epiphyseal ROI Segmentation
             └── Carpal & RUS Ossification Analysis
                         │
                         ▼
           [ Tanner-Whitehouse-CLI ]
           ├── TW3 RUS Score Calculation
           └── Skeletal Age (Months) & Z-Score
                         │
                         ▼
       [ Structured Pediatric Endocrine Summary JSON ]

2. Installation & Prerequisites

Ensure Python 3.10+, pydicom, torch, torchvision, and OpenCV are installed:

pip install pydicom torch torchvision opencv-python-headless numpy

Clone the offline model weights:

git clone https://github.com/OpenPHRorg/boneagenet-tw3-local.git
cd boneagenet-tw3-local

3. Radiogram Preprocessing & Hand Orientation Normalization

Standardize hand-wrist radiograms (flip right-to-left if necessary, crop background padding, and normalize intensity):

import pydicom
import cv2
import numpy as np

def preprocess_hand_radiogram(dicom_path):
    ds = pydicom.dcmread(dicom_path)
    pixel_array = ds.pixel_array.astype(float)
    
    # Invert photometric interpretation if MONOCHROME1
    if getattr(ds, 'PhotometricInterpretation', '') == 'MONOCHROME1':
        pixel_array = np.max(pixel_array) - pixel_array

    # 8-bit normalization
    norm_img = ((pixel_array - np.min(pixel_array)) / (np.max(pixel_array) - np.min(pixel_array)) * 255).astype(np.uint8)
    
    # Background threshold & bounding box crop
    _, thresh = cv2.threshold(norm_img, 15, 255, cv2.THRESH_BINARY)
    contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    if contours:
        c = max(contours, key=cv2.contourArea)
        x, y, w, h = cv2.boundingRect(c)
        cropped_img = norm_img[y:y+h, x:x+w]
    else:
        cropped_img = norm_img

    resized_img = cv2.resize(cropped_img, (512, 512))
    return resized_img

# Example preprocessing
prep_hand = preprocess_hand_radiogram("sample_hand_wrist.dcm")
cv2.imwrite("preprocessed_hand.png", prep_hand)
print("Hand radiogram preprocessing completed.")

4. Inference & Skeletal Maturity Assessment

Execute epiphyseal ROI segmentation and Tanner-Whitehouse (TW3) bone age estimation:

import torch

def estimate_bone_age(prep_image, sex, chronological_age_months):
    model = torch.hub.load('OpenPHRorg/boneagenet-tw3-local', 'boneage_tw3_v1', pretrained=True)
    model.eval()

    tensor_input = torch.from_numpy(prep_image).unsqueeze(0).unsqueeze(0).float() / 255.0
    sex_tensor = torch.tensor([[1.0 if sex.lower() == 'male' else 0.0]])

    with torch.no_grad():
        predicted_months_tensor, tw3_score_tensor = model(tensor_input, sex_tensor)
        predicted_months = float(predicted_months_tensor.item())
        tw3_score = float(tw3_score_tensor.item())

    age_diff_months = predicted_months - chronological_age_months
    variance_status = "Advanced (+12m)" if age_diff_months > 12 else "Delayed (-12m)" if age_diff_months < -12 else "Normal Range"

    return {
        "predicted_bone_age_months": round(predicted_months, 1),
        "predicted_bone_age_years": round(predicted_months / 12.0, 1),
        "tw3_rus_score": round(tw3_score, 1),
        "chronological_age_months": chronological_age_months,
        "age_variance_months": round(age_diff_months, 1),
        "skeletal_maturity_status": variance_status
    }

# Run diagnostic assessment
result = estimate_bone_age(prep_hand, sex="male", chronological_age_months=132)  # 11 years old
print(f"Pediatric Endocrine Summary: {result}")

5. Verification & Summary

This engine delivers instant, offline pediatric skeletal age assessments conforming to RSNA and Tanner-Whitehouse (TW3) standards.