bone_2026/README.md

19 KiB
Raw Blame History

🦴 DXA Quality Assessment

Сервис автоматизированного контроля качества денситометрических исследований (DXA): принимает DICOM, определяет анатомическую область, оценивает, пригодно ли изображение для клинической интерпретации, и формирует структурированный отчёт.

Что делает решение

Шаг Реализация
Определение анатомической области Ширина кадра (позвоночник / бедро) + голова области + геометрия яркой зоны
Бинарная оценка качества ResNet18 (ImageNet) → линейная голова; порог подобран по F1 на валидации
Тип нарушения Общая категория для снимков с нарушением; детальный тип требует разметки типов на уровне снимка
Отчёт 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

После запуска сервера:

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, геометрия кадра
              └──▶ эвристики: резкость, «плотные» включения

Ключевые решения и почему они такие:

  1. Линейный зонд вместо полного 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 оставлен для экспериментов на большем объёме данных.

  2. Метки из имён файлов. Суффикс _good/_bad — экспертная оценка снимка; отсутствие суффикса означает «изображение хорошее». Приоритет: _bad > _good > нет метки.

  3. Склейка побайтных дублей. В датасете 544 файла, но 252 уникальных снимка: один и тот же кадр сохранён многократно под разными именами (часть — с меткой, часть — без). Без склейки одно изображение попадало бы в оба класса.

  4. Разбиение по исследованиям. Снимки одного исследования не попадают одновременно в train и val — иначе метрики завышаются за счёт утечки.

  5. Аугментация отключена. Проверено экспериментально: яркостный разброс и сдвиг кадра снижают AUC с 0.87 до 0.56, потому что распределение яркости и положение области сами являются признаками качества. Флаг --augment включает её для экспериментов.

  6. Порог по логиту. При доле нарушений ~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 минут на исследование» выполняется с большим запасом.


Ограничения (важно для интерпретации)

  1. Разметка исходных данных — на уровне исследования, а не снимка. В наборе один снимок помечен _bad, остальные снимки того же исследования не размечены. Метка снимка считается унаследованной от исследования, поэтому часть меток заведомо шумная.

  2. Мало данных. 252 уникальных снимка, 37 нарушений. Доверительные интервалы широкие; оценка на закрытом наборе может отличаться.

  3. Тип нарушения определяется эвристиками, а не обученной моделью. Для честного мультикласса нужна разметка типов на уровне снимка.

  4. Область определяется по размеру кадра. Признак безошибочно работает на этом оборудовании (99/99 для позвоночника), но при смене аппарата порог SPINE_MIN_WIDTH потребует калибровки.

  5. В DICOM нет разметки ROI. Ни overlay, ни graphic annotation в файлах нет, поэтому корректность нанесённых областей измерения нельзя проверить прямым сравнением — оценивается только геометрия видимой зоны.

  6. Эвристики из src/quality/detailed_assessment.py не калиброваны. Пороги для «движения», «артефактов» и отступов ROI рассчитаны на другой масштаб интенсивностей: на этом наборе они срабатывают почти для любого снимка, а признак резкости ведёт себя противоположно в позвоночнике и бёдрах. Поэтому в панели деталей показываются числовые измерения с пометкой «справ.», а не вердикты «Да/Нет». Классификацию выполняет только модель. Проверить её вклад можно командой:

    python -m src.dxa.discriminator --model-path models/dxa_model.pth
    

Что проверено и как

Проверка Команда Результат
Модель использует снимок, а не только область python -m src.dxa.discriminator AUC 0.82 против 0.72 у правила «позвоночник = нарушение»; внутри областей у модели 0.84–0.95, у правила 0.50
Контракт API для веб-интерфейса python -m pytest tests/test_api_contract.py 15 тестов: поля панели деталей, различимость метрик, PNG-визуализации, отсутствие некалиброванных вердиктов
Разметка и разбиение данных python -m pytest tests/test_labels.py метки из имён, склейка дублей, отсутствие утечки между train/val
Метрики и порог python -m pytest tests/test_preprocess_and_model.py подбор порога при дисбалансе, roundtrip чекпоинта, BatchNorm
Веб-интерфейс в браузере node tests/browser/ui_check.js значения панели меняются при переключении строк
Работа без сети node tests/browser/ui_offline.js ноль внешних запросов, стили и иконки на месте

Браузерные проверки требуют запущенного сервера:

python -m uvicorn src.main:app --port 8123
node tests/browser/ui_check.js      # панель деталей обновляется по клику
node tests/browser/ui_offline.js    # работа без доступа к внешним сервисам

Офлайн-режим обеспечен локальными копиями Tailwind и FontAwesome (src/api/static/vendor, src/api/static/webfonts); страница не обращается к CDN.

План доработки

  • Разметить типы нарушений на уровне снимка и обучить мультилейбл-классификатор.
  • Собрать 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; отчёт удобно приложить к презентации.

Команда

  • Грачев Денис — разработка
  • Грачев Татьяна — капитан
Built for Bone Quality Assessment Hackathon 2026