16 KiB
🦴 DXA Quality Assessment
Сервис автоматизированного контроля качества денситометрических исследований (DXA): принимает DICOM, определяет анатомическую область, оценивает, пригодно ли изображение для клинической интерпретации, и формирует структурированный отчёт.
Что делает решение
| Шаг | Реализация |
|---|---|
| Определение анатомической области | Ширина кадра (позвоночник / бедро) + голова области + геометрия яркой зоны |
| Бинарная оценка качества | ResNet18 (ImageNet) → линейная голова; порог подобран по F1 на валидации |
| Тип нарушения | Эвристики по изображению: размытие/движение, посторонние включения, геометрия ROI |
| Отчёт | XLSX/CSV со столбцами из требований; опционально zip с визуализацией зоны интереса |
| API | FastAPI: анализ, детальный анализ, пакетная обработка, экспорт, DICOM SR (текст) |
| Веб-интерфейс | Загрузка DICOM, таблица результатов |
Установка и запуск
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
./run.sh train # обучить модель качества
./run.sh infer "dataset_hack/Для теста" results.xlsx # пакетная обработка
./run.sh serve # API и веб-интерфейс на :8000
./run.sh test # тесты
Для инференса только на CPU (образ меньше, без CUDA-колёс):
pip install -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cpu
Обучение и инференс можно вызывать напрямую:
python -m src.dxa.train --epochs 100 --output-dir models
python -m src.dxa.inference --input-path dataset_hack --output-path results.xlsx --zip-out masks.zip
После запуска сервера:
- Веб-интерфейс: http://localhost:8000
- Swagger UI: http://localhost:8000/docs
Docker
docker build -t dxa-quality .
docker run -v /path/to/data:/data -p 8000:8000 dxa-quality
Чекпоинт должен лежать в models/dxa_model.pth до сборки; путь задаётся переменной
DXA_MODEL_PATH (по умолчанию /app/models/dxa_model.pth). Сборка проверяет, что
чекпоинт читается, и падает, если модели нет — вместо тихих 500-х ответов в рантайме.
Вес модели внутрь образа зашит, из сети ничего не скачивается.
Архитектура
DICOM ──▶ предобработка ──▶ ResNet18 (заморожен) ──▶ линейная голова ──▶ логит
│ │
│ └──▶ голова области (вспомогательная)
│
├──▶ геометрия яркой зоны: область, ROI, геометрия кадра
└──▶ эвристики: резкость, «плотные» включения
Ключевые решения и почему они такие:
-
Линейный зонд вместо полного fine-tune. Уникальных снимков в наборе ~250. Полный fine-tune ResNet18 переобучается за несколько эпох (train F1 → 1.0 при val AUC ≈ 0.5). Замороженный backbone + линейная голова удерживает val AUC ≈ 0.7–0.85. Режим
--head mlp --freeze-epochs 0оставлен для экспериментов на большем объёме данных. -
Метки из имён файлов. Суффикс
_good/_bad— экспертная оценка снимка; отсутствие суффикса означает «изображение хорошее». Приоритет:_bad>_good> нет метки. -
Склейка побайтных дублей. В датасете 544 файла, но 252 уникальных снимка: один и тот же кадр сохранён многократно под разными именами (часть — с меткой, часть — без). Без склейки одно изображение попадало бы в оба класса.
-
Разбиение по исследованиям. Снимки одного исследования не попадают одновременно в train и val — иначе метрики завышаются за счёт утечки.
-
Аугментация отключена. Проверено экспериментально: яркостный разброс и сдвиг кадра снижают AUC с 0.87 до 0.56, потому что распределение яркости и положение области сами являются признаками качества. Флаг
--augmentвключает её для экспериментов. -
Порог по логиту. При доле нарушений ~15 % порог 0.5 даёт нулевой recall. Порог подбирается по F1 на валидации и сохраняется в чекпоинт; решение принимается по логиту (численно устойчиво при насыщении вероятностей).
Формат выходных данных
Основные столбцы соответствуют требованиям задания:
| Столбец | Описание |
|---|---|
path_to_study |
Путь к исследованию (для HTTP-загрузки — upload://<имя>) |
study_uid |
StudyInstanceUID |
image_uid |
SOPInstanceUID |
anatomical_region |
spine / hip_left / hip_right / hip |
quality_class |
0 — качественное, 1 — есть нарушение |
violation_type |
Тип нарушения или пустая строка |
processing_status |
Success или Failure: <причина> |
time_of_processing |
Время обработки, секунды |
Дополнительно добавляются confidence, violation_reason, region_confidence
— они не мешают автоматическому разбору обязательных столбцов.
API
| Метод | Путь | Назначение |
|---|---|---|
| GET | / |
Веб-интерфейс |
| GET | /api/v1/health |
Статус и признак загрузки модели |
| POST | /api/v1/analyze |
Базовый анализ одного файла |
| POST | /api/v1/analyze/detailed |
Расширенный отчёт, опционально маска |
| POST | /api/v1/analyze/sr |
Текстовое представление отчёта DICOM SR |
| POST | /api/v1/batch |
Пакетный анализ |
| POST | /api/v1/export |
Пакетный анализ и выгрузка в XLSX |
curl -X POST http://localhost:8000/api/v1/analyze -F "file=@study/spine.dcm"
{
"study_uid": "1.2.643...",
"image_uid": "1.2.643...",
"anatomical_region": "spine",
"quality_class": 1,
"quality_label": "Violation detected",
"violation_type": "quality_violation_detected",
"reason": "Выявлено нарушение качества изображения",
"confidence": 0.72,
"threshold_probability": 0.6154,
"processing_status": "Success"
}
Метрики
Метрики зависят от выбранного разбиения по исследованиям, поэтому приводятся с разбросом. Оценка на валидационной части (19 исследований, 51 снимок, 8 нарушений), разбиение по исследованиям:
| Что измерено | Значение | Как измерено |
|---|---|---|
| ROC-AUC, 5 разбиений | 0.76 ± 0.08 (0.64 – 0.84) | обучение по seed 0..4, порог по F1 |
| PR-AUC, 5 разбиений | 0.48 ± 0.17 | там же; базовый уровень при 15 % нарушений — 0.15 |
| F1, 5 разбиений | 0.54 ± 0.11 | там же (порог подобран на той же валидации — смещено вверх) |
| ROC-AUC, 5-фолдовая CV | 0.81 ± 0.08 | линейный зонд на тех же признаках, разбиение по исследованиям |
| Контрольная задача «позвоночник / бедро» | AUC 1.00 | проверка работоспособности пайплайна |
| Перестановка меток (нулевая гипотеза) | AUC 0.64 | вклад случайных корреляций |
Метрики по областям — в models/train_report.md, он создаётся при обучении.
Разбивка важна, потому что нарушения распределены крайне неравномерно: в
позвоночнике ~29 % снимков с нарушением против ~4–5 % у бёдер, а область почти
однозначно определяется по ширине кадра. Поэтому общий AUC частично отражает
различение области, а не только распознавание дефекта.
Время обработки одного снимка — порядка 0.02–0.05 с на CPU (ResNet18 с замороженным backbone), то есть требование «не более 3 минут на исследование» выполняется с большим запасом.
Ограничения (важно для интерпретации)
- Разметка исходных данных — на уровне исследования, а не снимка.
В наборе один снимок помечен
_bad, остальные снимки того же исследования не размечены. Метка снимка считается унаследованной от исследования, поэтому часть меток заведомо шумная. - Мало данных. 252 уникальных снимка, 37 нарушений. Доверительные интервалы широкие; оценка на закрытом наборе может отличаться.
- Тип нарушения определяется эвристиками, а не обученной моделью. Для честного мультикласса нужна разметка типов на уровне снимка.
- Область определяется по размеру кадра. Признак безошибочно работает на этом
оборудовании (99/99 для позвоночника), но при смене аппарата порог
SPINE_MIN_WIDTHпотребует калибровки. - В DICOM нет разметки ROI. Ни overlay, ни graphic annotation в файлах нет, поэтому корректность нанесённых областей измерения нельзя проверить прямым сравнением — оценивается только геометрия видимой зоны.
План доработки
- Разметить типы нарушений на уровне снимка и обучить мультилейбл-классификатор.
- Собрать 500+ исследований для устойчивых метрик и честной валидации.
- Подключить Grad-CAM для объяснения решения (модуль есть, но не интегрирован).
- Заменить порог по ширине кадра на калибровку по метаданным аппарата.
Структура проекта
bone_2026/
├── src/
│ ├── main.py # FastAPI: маршруты и загрузка модели
│ ├── dxa/ # действующий модуль оценки качества
│ │ ├── labels.py # разбор имён, метки, склейка дублей, сплит
│ │ ├── preprocess.py # DICOM -> тензор (общий для обучения и API)
│ │ ├── dataset.py # Dataset и DataLoader
│ │ ├── model.py # сеть, метрики, подбор порога
│ │ ├── train.py # обучение и отчёт
│ │ └── inference.py # пакетный инференс, определение области
│ ├── quality/ # эвристики (частично используются API)
│ ├── api/static/ # веб-интерфейс
│ └── model/, core/, pipeline/ # устаревшие модули, не подключены к API
├── models/dxa_model.pth # чекпоинт (+ train_report.md)
├── tests/ # pytest: метки, сплит, метрики, модель
├── dataset_hack/ # данные (в git не хранятся)
├── Dockerfile
├── requirements.txt
└── run.sh
Запуск обучения
python -m src.dxa.train --epochs 100 --output-dir models
| Параметр | По умолчанию | Описание |
|---|---|---|
--data-root |
dataset_hack |
Каталог датасета |
--annotation-path |
dataset_hack/НД_для_обучения/разметка.xlsx |
Excel с разметкой (только отчёт о расхождениях) |
--backbone |
resnet18 |
resnet18 / resnet34 |
--head |
linear |
linear (линейный зонд) / mlp |
--freeze-epochs |
-1 |
-1 — backbone заморожен всегда; 0 — обучать всю сеть |
--epochs, --batch-size, --learning-rate, --weight-decay |
100 / 16 / 3e-4 / 5e-2 | Оптимизация |
--balance |
none |
loss / sampler для компенсации дисбаланса |
--val-fraction, --seed |
0.2 / 42 | Разбиение по исследованиям |
--augment |
выключено | Включает яркостную аугментацию (ухудшает метрики, см. п. 5) |
--output-dir |
models |
Куда сохранять чекпоинт и отчёты |
--dry-run |
— | Проверить разбор данных и разбиение без обучения |
После обучения в --output-dir появляются dxa_model.pth, train_report.md
и train_report.json; отчёт удобно приложить к презентации.
Команда
- Грачев Денис — разработка
- Грачев Татьяна — капитан