bone_2026/QWEN.md

13 KiB
Raw Blame History

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).


Известные ограничения

  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

docker build -t dxa-quality .
docker run -v /path/to/data:/data -p 8000:8000 dxa-quality

Dockerfile ставит зафиксированные версии, копирует только src/, models/ и run.sh, проверяет чекпоинт на этапе сборки и имеет HEALTHCHECK. Данные и тесты в образ не попадают (.dockerignore).