Разработка AI для анализа КТ-снимков
КТ (компьютерная томография) — трёхмерная визуализация. Данные — стек из 200–600 двумерных срезов толщиной 0.5–5 мм. Ключевое отличие от рентгена: 3D объём, единицы Хаунсфилда (HU) как количественная мера плотности тканей. AI-задачи на КТ: сегментация органов, детекция узлов (лёгкие, печень, почки), стадирование опухолей, измерение объёмов.
3D обработка КТ-данных
import numpy as np
import torch
import nibabel as nib
from monai.transforms import (
Compose, LoadImaged, AddChanneld, Orientationd,
Spacingd, ScaleIntensityRanged, CropForegroundd,
ResizeWithPadOrCropd, ToTensord
)
class CTAnalysisSystem:
def __init__(self, model_path: str, task: str = 'lung_nodule'):
self.preprocessing = self._build_preprocessing(task)
self.model = self._load_model(model_path)
self.task = task
def _build_preprocessing(self, task: str) -> Compose:
if task == 'lung_nodule':
# Лёгочные узлы: lung window (-1200 to 600 HU)
hu_min, hu_max = -1200, 600
elif task == 'liver_tumor':
# Печень: soft tissue window (-150 to 250 HU)
hu_min, hu_max = -150, 250
else:
hu_min, hu_max = -1000, 1000
return Compose([
LoadImaged(keys=['image']),
AddChanneld(keys=['image']),
Orientationd(keys=['image'], axcodes='RAS'),
Spacingd(keys=['image'],
pixdim=(1.5, 1.5, 2.0), # ресемплинг до равного voxel spacing
mode='bilinear'),
ScaleIntensityRanged(
keys=['image'],
a_min=hu_min, a_max=hu_max,
b_min=0.0, b_max=1.0, clip=True
),
ToTensord(keys=['image'])
])
def analyze(self, nifti_path: str) -> dict:
data = {'image': nifti_path}
data = self.preprocessing(data)
volume = data['image'].unsqueeze(0) # [1, 1, D, H, W]
with torch.no_grad():
prediction = self.model(volume)
if self.task == 'lung_nodule':
return self._process_nodule_detection(prediction, data)
elif self.task == 'organ_segmentation':
return self._process_segmentation(prediction)
Сегментация органов: MONAI и nnU-Net
MONAI (Medical Open Network for AI) — PyTorch-фреймворк специально для медицинского CV:
from monai.networks.nets import UNet
from monai.losses import DiceCELoss
from monai.metrics import DiceMetric
# Стандартная 3D U-Net для КТ сегментации
model = UNet(
spatial_dims=3, # 3D
in_channels=1,
out_channels=14, # TotalSegmentator: 104 структуры, базовый вариант 14
channels=(16, 32, 64, 128, 256),
strides=(2, 2, 2, 2),
num_res_units=2,
dropout=0.1
)
criterion = DiceCELoss(
include_background=False,
to_onehot_y=True,
softmax=True
)
TotalSegmentator — готовая модель для сегментации 104 анатомических структур на КТ:
from totalsegmentator.python_api import totalsegmentator
# Автоматическая сегментация всех органов
output = totalsegmentator(
input='ct_scan.nii.gz',
output='segmentations/',
task='total',
fast=False # full quality
)
Детекция лёгочных узлов
Лёгочные узлы — первый признак рака лёгкого. Задача: найти все потенциальные узлы диаметром > 3 мм в 3D объёме.
class NoduleDetector:
def __init__(self, model_path: str,
min_nodule_mm: float = 3.0,
confidence_threshold: float = 0.5):
self.model = load_nodule_model(model_path)
self.min_size = min_nodule_mm
self.threshold = confidence_threshold
def detect(self, ct_volume: np.ndarray,
voxel_spacing: tuple) -> list[dict]:
"""
ct_volume: [D, H, W] в HU
voxel_spacing: (mm/voxel в каждом направлении)
"""
# Сегментация лёгкого для ограничения поиска
lung_mask = self._segment_lung(ct_volume)
# Детекция узлов только в области лёгкого
nodule_mask = self.model.predict(ct_volume * lung_mask)
# Извлечение отдельных узлов
nodules = self._extract_nodules(nodule_mask, voxel_spacing)
# Фильтрация по минимальному размеру
return [n for n in nodules
if n['diameter_mm'] >= self.min_size and
n['confidence'] >= self.threshold]
Количественный анализ
Уникальная возможность КТ — точное измерение объёмов. После сегментации:
def measure_volume_ml(mask: np.ndarray,
voxel_spacing: tuple) -> float:
"""Объём сегментированной структуры в мл (cm³)"""
voxel_volume_mm3 = np.prod(voxel_spacing)
volume_mm3 = mask.sum() * voxel_volume_mm3
return volume_mm3 / 1000 # мм³ → мл
def measure_nodule_diameter(nodule_mask: np.ndarray,
voxel_spacing: tuple) -> dict:
"""Диаметр узла по методу RECIST"""
coords = np.where(nodule_mask)
# Longest axis в 3D
from scipy.spatial import ConvexHull
points = np.column_stack(coords) * np.array(voxel_spacing)
if len(points) < 4:
return {'diameter_mm': 0}
hull = ConvexHull(points)
max_dist = 0
hull_pts = points[hull.vertices]
for i in range(len(hull_pts)):
for j in range(i+1, len(hull_pts)):
d = np.linalg.norm(hull_pts[i] - hull_pts[j])
max_dist = max(max_dist, d)
return {'diameter_mm': round(max_dist, 2)}
| Задача | Датасет | Dice SOTA |
|---|---|---|
| Сегментация лёгких | LUNA16 | 0.98 |
| Детекция узлов | LUNA16 | FROC 0.89 |
| Сегментация печени | LiTS | 0.96 |
| Сегментация опухоли печени | LiTS | 0.75 |
| Multi-organ | BTCV | 0.88 |
| Задача | Срок |
|---|---|
| Сегментация органов (TotalSegmentator fine-tune) | 8–14 недель |
| Детекция лёгочных узлов | 14–20 недель |
| Полный CAD КТ-комплекс | 24–36 недель |







