bone_2026/QWEN.md

24 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       # имена -> метки, склейка дублей, разбиение, фиксация сплита
├── excel_labels.py # разметка снимков по экспертной таблице (labels_images.csv)
├── rename_files.py # приведение имён DICOM к виду область_NN[_метка]
├── violations.py   # единый словарь типов нарушений (коды, подписи, коды SR)
├── model_card.py   # карточка решения: разметка, данные, метрики, ограничения
├── compare_labels.py # сравнение источников разметки на одном held-out наборе
├── render.py       # рендер DICOM в PNG и контактные листы (для ручной проверки)
├── preprocess.py   # DICOM -> CHW-тензор (единый путь для обучения и API)
├── dataset.py      # DXADataset, DataLoader
├── model.py        # сеть, метрики, подбор порога, сохранение/загрузка
├── train.py        # обучение + отчёт (md/json)
└── inference.py    # пакетный инференс, определение области, визуализация

Артефакты вне кода: labels/labels_images.csv|.xlsx (официальная разметка), labels/labels_images_{table,union,expert}.csv (варианты правила и эталон), labels/split_expert_seed42.json (зафиксированное разбиение), labels/rename_map.csv (карта переименований), assets/labeling.md (как построена разметка и как выбиралось правило). Чекпоинты прогонов (models/archive/, models/compare_rules/, models/compare_labels/, models/excel_labels/) удалены 2026-09-27 — остались только числа в assets/labeling.md; пересобрать можно через ./run.sh split && ./run.sh compare.

Ключевые решения (проверены экспериментально)

Решение Причина
Единый словарь типов нарушений (src/dxa/violations.py) Коды, подписи и коды SR были в трёх копиях (инференс, main.py, dxa-app.js) и не знали кодов экспертной таблицы. Теперь подписи отдаёт сервер, фронт копий не держит
Метки только из экспертной таблицы (правило table): labels/labels_images.csv, 77 нарушений Таблица описывает исследование, но каждая область встречается в нём один раз, поэтому вердикт переносится на снимок однозначно. Правило выбрано измерением: учёт ручных пометок из имён файлов дал ROC-AUC 0.6199 против 0.6764, хуже на всех 5 seed'ах. См. assets/labeling.md
Склейка побайтных дублей 544 файла, но 252 уникальных снимка; без склейки снимок попадал в оба класса
Разбиение по исследованиям, не по снимкам Исключение утечки: снимки одного исследования в одной части
Линейный зонд (замороженный backbone) Полный fine-tune при ~250 снимках переобучается (val AUC → 0.5)
Порог по логиту, подбор по F1 При доле нарушений около 30 % порог 0.5 даёт почти нулевой recall; вероятности насыщаются
Аугментация выключена по умолчанию Яркость и положение сами являются признаками качества: AUC 0.87 → 0.56
Область по ширине кадра Позвоночник 300 px, бедро 280 px; 99/99 для позвоночника
Панель деталей показывает измерения, а не вердикты Эвристики detailed_assessment не калиброваны: motion_detected/any_detected истинны почти всегда, ROI-отступы срабатывают для 227/252 снимков

Проверка вклада модели

Не является ли модель просто детектором анатомии (нарушений около трети и в позвоночнике, и у бёдер, а область почти однозначно определяется по ширине кадра):

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

Результат на рабочем чекпоинте models/dxa_model.pth (оценка на всём наборе, включая обучающие снимки, поэтому значения смещены вверх):

Предиктор Общий AUC spine hip_right hip_left
Модель 0.854 0.928 0.861 0.853
Правило «позвоночник = нарушение» 0.529 0.500 0.500 0.500

Модель использует содержимое снимка: внутри областей она даёт 0.85–0.93. Правило по области внутри области всегда 0.50 (подсказки нет). Честная оценка на held-out — в assets/labeling.md §8: ROC-AUC 0.6764 [0.6309, 0.7218] по пяти seed'ам.

Данные (dataset_hack/)

Структура:

dataset_hack/
├── Для теста/              # bad.dcm, l_hip.dcm, r_hip.dcm, spine.dcm
└── НД_для_обучения/
    ├── разметка.xlsx       # экспертная оценка на уровне ИССЛЕДОВАНИЯ
    └── Исследования/<study_uid>/.../<region>_<n>[_good|_bad].dcm

Факты, важные для обучения:

  • 544 файла на диске, но 252 уникальных снимка (по пиксельному содержимому) на 100 исследований: позвоночник 99, бедро R 79, бедро L 73, 1 с неопределённой областью.
  • Экспертная таблица отмечает нарушения у 74 снимков (29.4 %); три снимка таблица область не оценивала.
  • Рабочая разметка (правило table) — 77 нарушений из 252 (30.6 %): 74 по таблице плюс 3 снимка без экспертной оценки, помеченных filename_fallback.
  • Дубли не пересекают границы исследований, конфликтов меток при склейке нет. Два побайтных дубля названы по-разному, поэтому область определяется голосованием по именам файлов.
  • Имена файлов приведены к виду <область>_<NN>[_good|_bad].dcm (spine, l_hip, r_hip); инструмент — src/dxa/rename_files.py, карта отката — labels/rename_map.csv. Суффиксы _good/_bad проставлялись вручную, в метках не участвуют — только как диагностический столбец quality_from_filename: они расходились с оценкой эксперта в 15 случаях из 252.
  • В DICOM нет разметки ROI (ни OverlayData, ни GraphicAnnotationSequence) и пусты теги Laterality/ImageLaterality, поэтому ни корректность областей, ни сторону бедра нельзя проверить по метаданным.
  • Столбец study в таблице — имя каталога исследования, а не StudyInstanceUID из DICOM (в датасете они разные, соответствие 1:1).

Единица разметки

Таблица описывает исследование, а не снимок. Однако каждая анатомическая область встречается в исследовании ровно один раз (после склейки дублей), поэтому вердикт исследования по области переносится на снимок однозначно — не нужно решать, какой из нескольких снимков «плохой». Так получены метки и типы нарушений (labels/labels_images.csv); каждый источник свидетельства сохранён в отдельном столбце, поэтому правило можно переиграть без повторного разбора. Почему выбрано именно правило «только таблица» — в assets/labeling.md §4.


Обучение

./run.sh label                                  # построить разметку снимков по Excel
./run.sh rename                                 # план приведения имён файлов (--apply)
./run.sh train                                  # режим по умолчанию (метки из таблицы)
python -m src.dxa.train --dry-run               # проверить данные без обучения
python -m src.dxa.train --labels-csv "" --dry-run   # режим меток из имён файлов
python -m src.dxa.train --head mlp --freeze-epochs 0 --epochs 30

Источник меток — --labels-csv (по умолчанию labels/labels_images.csv с правилом table; пустая строка возвращает метки из имён файлов, отсутствующий файл — откат к ним с предупреждением). Разбиение фиксируется (--export-split / --split-file), чтобы сравнивать варианты на одном held-out наборе:

./run.sh split && ./run.sh compare              # выбор правила разметки

split стратифицирует по эталону (labels/labels_images_expert.csv), compare прогоняет 5 seed'ов × 2 варианта и оценивает оба по этому же эталону; отчёты (comparison.md / comparison.json) и чекпоинты прогонов пишутся в --output-root (по умолчанию models/compare_labels) и в репозиторий не входят. Возобновить без переобучения — ./run.sh compare --skip-training.

Артефакты в --output-dir: dxa_model.pth (веса, порог, параметры предобработки), train_report.md, train_report.json.

Чекпоинт самодостаточен: backbone, head, preprocess, threshold, а также labels_csv и split_file хранятся внутри, поэтому инференс не может рассинхронизироваться с обучением, а по файлу видно, на какой разметке он обучен.

Рабочий чекпоинт — models/dxa_model.pth (правило table, seed 42 по умолчанию, эпоха 39, порог логита −0.4930 → вероятность 0.379; val ROC-AUC 0.6706 при честной оценке 0.6764 [0.6309, 0.7218] по пяти seed'ам). Это единственный чекпоинт в репозитории: прежние версии и прогоны сравнения удалены, откатиться можно только переобучением.


API (src/main.py)

Метод Путь Назначение
GET / Веб-интерфейс
GET /api/v1/health Статус, признак загрузки модели и её происхождение (разметка, разбиение, порог, эпоха)
GET /api/v1/model Карточка решения: разметка, данные, метрики с интервалами, словарь нарушений, ограничения
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        # 209 тестов
  • tests/test_labels.py — разбор имён, склейка дублей, отсутствие утечки при разбиении, фиксация разбиения в файле (export_split / load_split).
  • tests/test_rename_files.py — приведение имён: разбор, поиск свободного номера при конфликте, отказ от угадывания области, цикл «применить → откатить».
  • tests/test_excel_labels.py — разметка по экспертной таблице: чтение критериев, «1 = нарушение», голосование по области, перенос оценки на единственное бедро, три правила метки (table / union / expert) и их согласованность, подключение к обучению.
  • tests/test_violations.py — единый словарь типов: коды и подписи, коды SR, приведение устаревших значений, согласованность с критериями таблицы.
  • 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 — страница не обращается к внешним хостам.

Честность интерфейса

Веб-интерфейс не должен утверждать больше, чем известно решению, поэтому:

  • тип нарушения показан с пометкой «эвристика» и пояснением, что модель решает только бинарную задачу;
  • ROC-AUC рабочего чекпоинта в баннере состояния помечена как завышенная (он выбран лучшим из пяти seed'ов), а честная оценка лежит в панели «О модели»;
  • плитка средней уверенности не называется точностью;
  • подписи типов и метрики приходят с сервера (/api/v1/model), копий в JS нет.

Офлайн-работа фронтенда

Tailwind и FontAwesome лежат локально (src/api/static/vendor, src/api/static/webfonts), страница не обращается к CDN. Требование методики — работа без внешних сервисов; CDN-версии ломали оформление в закрытом контуре. Наличие ассетов проверяется на этапе сборки образа (см. Dockerfile).


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

  1. Разметка снимков выведена из таблицы, описывающей исследование: поштучной экспертной оценки снимков в наборе нет. Оценка качества модели упирается в качество этой разметки, а не только в объём данных.
  2. Мало данных: 252 снимка, 77 нарушений; доверительные интервалы широкие (ROC-AUC 0.6764 [0.6309, 0.7218] по пяти seed'ам).
  3. Эталон оценки — та же экспертная таблица, независимой истины нет; сравнение правил разметки частично благоприятствует варианту «только таблица».
  4. Тип нарушения определяется эвристиками, а не обученной моделью; 5 снимков имеют только unspecified, потому что источник не указывает критерий.
  5. Три снимка без экспертной оценки размечены по пометке в имени файла и помечены filename_fallback.
  6. Сторона бедра в 7 исследованиях с единственным снимком не проверяема: теги Laterality пусты, оценка взята из единственного заполненного столбца.
  7. Порог SPINE_MIN_WIDTH привязан к текущему оборудованию.

Удалённый устаревший код

Не подключённые к API модули (src/core/orchestrator.py, src/pipeline/, src/model/{unet,segmentator}.py, src/models/*, src/classifiers/, src/segmentators/, src/dataloaders/, src/training/, src/api/{endpoints,annotation,root,schemas}, src/quality/{artifact_detector, position_validator,universal_scorer,medical_quality}.py) удалены 2026-09-27 вместе с неиспользуемыми чекпоинтами (quality_classifier.pth, region_detector.pth, violation_classifier.pth, dxa_model_final.pth) и папками docs/, tools/, public/.

Из живого кода их тянул только src/__init__.py — реэкспорт Config, FlexibleDataset, UNet, QualityScorer; теперь в нём остался только __version__, а src/quality/__init__.py — только докстрока. При добавлении новых модулей в src/quality/ помнить, что пакет больше ничего не импортирует.


Docker

docker compose up -d                          # CPU, http://localhost:8000
docker compose --profile cuda up -d dxa-cuda  # GPU (нужен nvidia-container-toolkit)

Образы: Dockerfile (python:3.11-slim, torch из CPU-индекса) и Dockerfile_cuda (та же база, torch и torchvision из индекса cu126 — под CUDA 11.8 колёс torch 2.8.0 нет, индекс заканчивается на 2.7.1). Оба собираются из src/, requirements.txt, run.sh и чекпоинта: models/dxa_model.pth отслеживается git и копируется в /app/models на этапе сборки, поэтому монтировать пути при запуске не нужно (условие приёмки — модели уже внутри образа). Сборка проверяет импорт приложения, наличие офлайн-ассетов фронтенда и читаемость чекпоинта, образ имеет HEALTHCHECK. Данные, тесты и labels/ в образ не попадают (.dockerignore), поэтому ./run.sh train внутри контейнера возьмёт метки из имён файлов (с предупреждением); для обучения в контейнере смонтируйте labels/ или передайте свой --labels-csv. Если внешний docker-swarm.yml монтирует каталог с моделями в /app/models, монтирование перекроет встроенный чекпоинт.