13 KiB
Bone Quality Assessment Project
Project Overview
Медицинский ИИ-сервис для автоматизированной оценки качества денситометрических исследований (DXA). Принимает DICOM, определяет анатомическую область, решает бинарную задачу «качественное изображение / есть нарушение» и формирует отчёт.
Ключевые требования (condition.txt)
- Области: поясничный отдел позвоночника и проксимальный отдел бедренной кости.
- Обязательна контейнеризация и скрипт сборки/запуска в Linux.
- Результат: XLSX/CSV, одна строка на изображение, столбцы
path_to_study, study_uid, image_uid, anatomical_region, quality_class, violation_type, processing_status, time_of_processing. - Приоритетные метрики: F1 и ROC-AUC (с 95 % доверительными интервалами).
- Работа офлайн, без передачи изображений во внешние сервисы.
- Время обработки одного исследования — не более 3 минут.
Технологии
| Компонент | Технология |
|---|---|
| Backend | Python, FastAPI, Uvicorn |
| ML | PyTorch, torchvision (ResNet18) |
| Изображения | pydicom, Pillow, OpenCV, SciPy |
| Данные | pandas, openpyxl, scikit-learn |
| Тесты | pytest |
Действующая архитектура
Всё, что реально работает, находится в src/dxa/ и src/main.py.
src/dxa/
├── labels.py # имена -> метки, склейка дублей, разбиение по исследованиям
├── preprocess.py # DICOM -> CHW-тензор (единый путь для обучения и API)
├── dataset.py # DXADataset, DataLoader
├── model.py # сеть, метрики, подбор порога, сохранение/загрузка
├── train.py # обучение + отчёт (md/json)
└── inference.py # пакетный инференс, определение области, визуализация
Ключевые решения (проверены экспериментально)
| Решение | Причина |
|---|---|
Метки из имён файлов: _bad > _good > нет метки (=good) |
Явная оценка в имени файла; отсутствие метки означает «хорошее» |
| Склейка побайтных дублей | 544 файла, но 252 уникальных снимка; без склейки снимок попадал в оба класса |
| Разбиение по исследованиям, не по снимкам | Исключение утечки: снимки одного исследования в одной части |
| Линейный зонд (замороженный backbone) | Полный fine-tune при ~250 снимках переобучается (val AUC → 0.5) |
| Порог по логиту, подбор по F1 | При 15 % нарушений порог 0.5 даёт нулевой recall; вероятности насыщаются |
| Аугментация выключена по умолчанию | Яркость и положение сами являются признаками качества: AUC 0.87 → 0.56 |
| Область по ширине кадра | Позвоночник 300 px, бедро 280 px; 99/99 для позвоночника |
| Панель деталей показывает измерения, а не вердикты | Эвристики detailed_assessment не калиброваны: motion_detected/any_detected истинны почти всегда, ROI-отступы срабатывают для 227/252 снимков |
Проверка вклада модели
Не является ли модель просто детектором анатомии (в позвоночнике ~29 % нарушений против ~4–5 % у бёдер, а область почти однозначно определяется по ширине кадра):
python -m src.dxa.discriminator --model-path models/dxa_model.pth
Результат на чекпоинте models/dxa_model.pth:
| Предиктор | Общий AUC | spine | hip_right | hip_left |
|---|---|---|---|---|
| Модель | 0.822 | 0.838 | 0.895 | 0.948 |
| Правило «позвоночник = нарушение» | 0.724 | 0.500 | 0.500 | 0.500 |
Модель использует содержимое снимка: внутри областей она даёт 0.84–0.95. Правило по области внутри области всегда 0.50 (подсказки нет).
Данные (dataset_hack/)
Структура:
dataset_hack/
├── Для теста/ # bad.dcm, l_hip.dcm, r_hip.dcm, spine.dcm
└── НД_для_обучения/
├── разметка.xlsx # экспертная оценка на уровне ИССЛЕДОВАНИЯ
└── Исследования/<study_uid>/.../<region>_<n>[_good|_bad].dcm
Факты, важные для обучения:
- 544 файла на диске, но 252 уникальных снимка (по пиксельному содержимому).
- 86 файлов имеют явную метку; после склейки дублей — 37 нарушений из 252 (14.7 %).
- Дубли не пересекают границы исследований, конфликтов меток при склейке нет.
- Имена неоднородны:
spine_01,Spine,r_spine,spine-1,l_hip,l_hip-2,r_hip,r_hop. - В DICOM нет разметки ROI (ни OverlayData, ни GraphicAnnotationSequence), поэтому корректность нанесённых областей нельзя проверить прямым сравнением.
- Метка в Excel относится к исследованию и раздаётся его снимкам; имена файлов имеют приоритет. Excel используется только для предупреждения о расхождениях.
Единица разметки — источник шума
Один снимок в исследовании помечен _bad, остальные не размечены. Метка снимка
считается унаследованной от исследования, поэтому часть меток заведомо шумная.
Это главное ограничение текущего качества модели.
Обучение
./run.sh train # режим по умолчанию
python -m src.dxa.train --dry-run # проверить данные без обучения
python -m src.dxa.train --head mlp --freeze-epochs 0 --epochs 30
Артефакты в --output-dir: dxa_model.pth (веса, порог, параметры
предобработки), train_report.md, train_report.json.
Чекпоинт самодостаточен: backbone, head, preprocess, threshold_logit
хранятся внутри, поэтому инференс не может рассинхронизироваться с обучением.
API (src/main.py)
| Метод | Путь | Назначение |
|---|---|---|
| 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 |
Путь к модели — переменная окружения DXA_MODEL_PATH (по умолчанию
models/dxa_model.pth), чтобы контейнер не зависел от рабочего каталога.
API и CLI используют один код предсказания (predict_from_bytes /
predict_from_array), поэтому предобработка и порог совпадают.
Тесты
./run.sh test
python -m pytest tests/ -q # 79 тестов
tests/test_labels.py— разбор имён, склейка дублей, отсутствие утечки при разбиении.tests/test_preprocess_and_model.py— предобработка, метрики, подбор порога, контракт модели, BatchNorm при заморозке, roundtrip чекпоинта.tests/test_api_contract.py— поля ответов, которые читаетdxa-app.js(панель деталей ранее показывала прочерки из-за расхождения ключей), различимость метрик между снимками, валидность PNG-визуализаций, отсутствие некалиброванных вердиктов в ответе.
Проверка веб-интерфейса в браузере
tests/browser/*.js (Node + playwright-core из bundled Browser Use) открывают
интерфейс, загружают DICOM, кликают по строкам таблицы и снимают содержимое
панели деталей. Используется временный профиль Chrome, профиль пользователя не
затрагивается. Требуется запущенный сервер на 127.0.0.1:8123.
ui_check.js— значения панели меняются при переключении строк.ui_violation.js— ветка «нарушение» (бейдж, POOR, HIGH, заключение).ui_offline.js— страница не обращается к внешним хостам.
Офлайн-работа фронтенда
Tailwind и FontAwesome лежат локально (src/api/static/vendor,
src/api/static/webfonts), страница не обращается к CDN. Требование методики —
работа без внешних сервисов; CDN-версии ломали оформление в закрытом контуре.
Наличие ассетов проверяется на этапе сборки образа (см. Dockerfile).
Известные ограничения
- Разметка на уровне исследования → шум в метках снимков.
- Мало данных: 252 снимка, 37 нарушений; доверительные интервалы широкие.
- Тип нарушения определяется эвристиками, а не обученной моделью.
- Порог
SPINE_MIN_WIDTHпривязан к текущему оборудованию. - Grad-CAM (
src/models/visualization/gradcam.py) есть, но не подключён.
Устаревший код (не подключён к API)
Эти модули не импортируются из src/main.py и src/dxa/*; их зависимости
закомментированы в requirements.txt:
src/api/endpoints.py— падает при импорте, роутер не монтируется.src/api/annotation.py— маршруты под/api/annotation, не монтируются.src/core/orchestrator.py,src/pipeline/pipeline.py— веса не загружаются.src/model/unet.py,src/model/segmentator.py— UNet-заглушки.src/quality/artifact_detector.py,position_validator.py,medical_quality.py— заглушки.src/dataloaders/pet_dataset.py— остаток прототипа (Oxford-IIIT Pet).
Docker
docker build -t dxa-quality .
docker run -v /path/to/data:/data -p 8000:8000 dxa-quality
Dockerfile ставит зафиксированные версии, копирует только src/, models/ и
run.sh, проверяет чекпоинт на этапе сборки и имеет HEALTHCHECK. Данные и тесты
в образ не попадают (.dockerignore).