185 lines
11 KiB
Markdown
185 lines
11 KiB
Markdown
# 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 для позвоночника |
|
||
|
||
### Метрики (валидация, разбиение по исследованиям)
|
||
|
||
- ROC-AUC по 5 разбиениям: **0.76 ± 0.08** (0.64 – 0.84)
|
||
- 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
|
||
|
||
Разбивка по областям обязательна: в позвоночнике ~29 % нарушений против ~4–5 %
|
||
у бёдер, а область почти однозначно определяется по ширине кадра, поэтому общий
|
||
AUC частично отражает различение области.
|
||
|
||
---
|
||
|
||
## Данные (`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`, остальные не размечены. Метка снимка
|
||
считается унаследованной от исследования, поэтому часть меток заведомо шумная.
|
||
Это главное ограничение текущего качества модели.
|
||
|
||
---
|
||
|
||
## Обучение
|
||
|
||
```bash
|
||
./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`), поэтому предобработка и порог совпадают.
|
||
|
||
---
|
||
|
||
## Тесты
|
||
|
||
```bash
|
||
./run.sh test
|
||
python -m pytest tests/ -q # 65 тестов
|
||
```
|
||
|
||
`tests/test_labels.py` — разбор имён, склейка дублей, отсутствие утечки при
|
||
разбиении. `tests/test_preprocess_and_model.py` — предобработка, метрики,
|
||
подбор порога, контракт модели, BatchNorm при заморозке, roundtrip чекпоинта.
|
||
|
||
---
|
||
|
||
## Известные ограничения
|
||
|
||
1. Разметка на уровне исследования → шум в метках снимков.
|
||
2. Мало данных: 252 снимка, 37 нарушений; доверительные интервалы широкие.
|
||
3. Тип нарушения определяется эвристиками, а не обученной моделью.
|
||
4. Порог `SPINE_MIN_WIDTH` привязан к текущему оборудованию.
|
||
5. 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
|
||
|
||
```bash
|
||
docker build -t dxa-quality .
|
||
docker run -v /path/to/data:/data -p 8000:8000 dxa-quality
|
||
```
|
||
|
||
Dockerfile ставит зафиксированные версии, копирует только `src/`, `models/` и
|
||
`run.sh`, проверяет чекпоинт на этапе сборки и имеет HEALTHCHECK. Данные и тесты
|
||
в образ не попадают (`.dockerignore`).
|