diff --git a/assets/img/architecture-region-detection.png b/assets/img/architecture-region-detection.png index 4784a76..a5e8f16 100644 Binary files a/assets/img/architecture-region-detection.png and b/assets/img/architecture-region-detection.png differ diff --git a/docs/LTsT_zadacha_4_komanda_Graitech.pptx b/docs/LTsT_zadacha_4_komanda_Graitech.pptx new file mode 100644 index 0000000..57439b6 Binary files /dev/null and b/docs/LTsT_zadacha_4_komanda_Graitech.pptx differ diff --git a/docs/technical-description.html b/docs/technical-description.html new file mode 100644 index 0000000..b123fb4 --- /dev/null +++ b/docs/technical-description.html @@ -0,0 +1,998 @@ + + + + +Техническое описание — автоматизированная оценка качества DXA + + + + +
+

Автоматизированная оценка качества денситометрических исследований

+
Техническое описание решения
+ + + + + + + + +
ЗадачаКонтроль качества DXA-исследований: бинарная оценка «качественное / есть нарушение», определение анатомической области, тип нарушения, структурированный отчёт
Дата документа27 сентября 2026 г.
КомандаГрачев Денис — разработка; Грачев Татьяна — капитан
Версия решения1.0.0 (src/__init__.py)
Рабочий чекпоинтmodels/dxa_model.pth: ResNet18 + линейная голова, разметка table, seed 42, эпоха 39, порог логита −0.4930 (вероятность 0.379)
Ключевые метрикиROC-AUC 0.6764 [0.6309, 0.7218], F1 0.5676 [0.5270, 0.6082] — пять seed'ов на фиксированном разбиении по исследованиям
Время обработки0.021 с на изображение (медиана, CPU), 548 файлов за 12.1 с; требование «≤ 3 мин на исследование» выполняется с запасом
+
+ +

Документ описывает фактическое состояние репозитория на дату, указанную на титуле: +все параметры, пороги, метрики и имена полей приведены по коду и по выполненным измерениям, а не по +проектным намерениям. Там, где значение измерено на конкретной машине, указано, на какой. +Расхождения и ограничения перечислены явно — разделы 15 и 16.

+ +
+

Содержание

+
    +
  1. 1. Назначение и область применения
  2. +
  3. 2. Соответствие требованиям задания
  4. +
  5. 3. Архитектура решения
  6. +
  7. 4. Состав данных и разметка
  8. +
  9. 5. Предобработка изображений
  10. +
  11. 6. Модель и процедура обучения
  12. +
  13. 7. Определение анатомической области
  14. +
  15. 8. Таксономия нарушений качества
  16. +
  17. 9. Форматы входных и выходных данных
  18. +
  19. 10. API сервиса
  20. +
  21. 11. Веб-интерфейс
  22. +
  23. 12. Метрики качества
  24. +
  25. 13. Производительность и системные требования
  26. +
  27. 14. Сборка, запуск и развёртывание
  28. +
  29. 15. Тесты и проверки
  30. +
  31. 16. Известные ошибки и их обработка
  32. +
  33. 17. Ограничения и достоверность результатов
  34. +
  35. 18. План развития
  36. +
  37. Приложение А. Структура репозитория
  38. +
  39. Приложение Б. Команды
  40. +
+
+ +

1. Назначение и область применения

+ +

Сервис принимает денситометрическое исследование в формате DICOM и выполняет автоматизированный +контроль его качества: определяет анатомическую область, решает бинарную задачу «изображение пригодно +для клинической интерпретации / содержит нарушение качества», относит нарушение к категории и +формирует структурированный отчёт (XLSX/CSV) с одной строкой на изображение.

+ +

Задача решается как цифровой помощник, а не как замена врача: результат предназначен для отбора +исследований, требующих внимания специалиста, и для единообразной регистрации нарушений. Области +анализа — поясничный отдел позвоночника и проксимальный отдел бедренной кости (левая и правая +стороны рассматриваются как отдельные снимки, поскольку сторона влияет на трактовку критериев).

+ +

Решение работает полностью офлайн: изображения не передаются во внешние сервисы, все зависимости и +веса фиксированы, доступ в сеть при инференсе не требуется. Это прямое требование методики +(п. 3.2 задания) и одновременно условие применимости в закрытом контуре медицинской организации.

+ +

2. Соответствие требованиям задания

+ +

Таблица связывает пункты задания с реализацией и со способом проверки. Формулировки требований +приведены по тексту методических рекомендаций.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ТребованиеРеализацияЧем подтверждается
Области: поясничный отдел позвоночника и проксимальный отдел бедренной костиОбласть определяется автоматически по кадру; бедро разделяется на левое и правоеРазд. 7; на валидации 99/99 для позвоночника
Бинарная классификация «качественное / есть нарушение»ResNet18 (ImageNet) + линейная голова, порог по логиту, подобранный по F1Разд. 6, 12; tests/test_preprocess_and_model.py
Определение типа нарушенияЕдиный словарь кодов src/dxa/violations.py; отнесение к категории — по измеряемым признакам снимкаРазд. 8; tests/test_violations.py
Оценка корректности разметки анатомических структурВ наборе нет ROI-разметки в DICOM, поэтому оценивается геометрия видимой зоны измерения и её границыРазд. 8, 17 (ограничение 4)
Отчёт .xlsx/.csv, одна строка на изображение, столбцы из п. 2.5CLI-инференс и POST /api/v1/export; обязательные восемь столбцов плюс три диагностическихРазд. 9 (состав столбцов проверен на реальном прогоне)
Время обработки одного исследования ≤ 3 минОдин снимок — один прямой проход сети 224×224, без итеративных процедурРазд. 13: 0.021 с медиана на CPU, запас более чем трёхкратный
Отсутствие необработанных исключений; ошибки фиксируются в отчётеОшибка на файле не прерывает пакет: строка получает processing_status = Failure: …Разд. 16
Воспроизводимость при повторном запускеФиксированный seed, зафиксированное разбиение в файле, версии зависимостей, самодостаточный чекпоинтРазд. 6, 14; tests/test_labels.py
Пакетная обработка архива + общая таблица + zip с дополнительными сериямиОбход каталога, лог прогресса, XLSX/CSV, опциональный zip с визуализациейРазд. 9, 13
API для пакетной обработки тестового набораFastAPI: восемь маршрутов, включая /api/v1/batch и /api/v1/exportРазд. 10
Обязательная контейнеризация, скрипт сборки и запуска в LinuxDockerfile (python:3.11-slim), Dockerfile_cuda для GPU, run.shРазд. 14; сборка проверена, см. 14
Фиксация зависимостей, включая базовый контейнерrequirements.txt с точными версиями, базовый образ по тегу, torch с CPU-индексомРазд. 14
Работа без обращения к внешним сервисамОфлайн-ассеты фронтенда, отсутствие сетевых вызовов в коде предсказанияtests/browser/ui_offline.js
Полный комплект документацииREADME.md (обзор и воспроизведение), assets/labeling.md (разметка и её обоснование), docs/technical-description.html и .pdf (данный документ)—
+ +

3. Архитектура решения

+ +

3.1. Слои

+ + + + + + + + + + + + + +
СлойФайлыОтветственность
Прикладной ядерный слойsrc/dxa/Чтение DICOM, предобработка, модель, метрики, определение области, разметка данных, обучение, пакетный инференс
Сервисsrc/main.pyFastAPI: загрузка чекпоинта при старте, маршруты анализа, пакетной обработки, экспорта, здоровья и карточки решения
Веб-интерфейсsrc/api/static/Загрузка файлов, таблица результатов, панель деталей, панель «О модели»; все подписи и метрики получает с сервера
Эвристики качестваsrc/quality/Числовые измерения снимка (резкость, плотные включения, полнота и поворот области) для панели деталей
Инфраструктураrun.sh, Dockerfile, Dockerfile_cuda, JenkinsfileЕдиная точка входа для сборки, обучения, инференса и тестов; сборка образа и выкладка
+ +

3.2. Поток обработки одного изображения

+ +

Ниже — фактический путь данных от файла до строки отчёта. Ключевое свойство: обучение и сервис +используют один и тот же код предобработки и предсказания, поэтому расхождение между +офлайн-оценкой и работой API невозможно по построению, а не по договорённости.

+ +
+ + + + + + + + + + + + + + + + +
DICOM→Чтение пикселей и метаданных→Предобработка 224×224→ResNet18 + линейная голова
Логит → порог→Область по кадру→Числовые измерения→Строка отчёта / ответ API
+
+ +

Порядок операций и точка принятия решения:

+
    +
  1. Чтение. Из файла берутся пиксельные данные и идентификаторы исследования и снимка + (StudyInstanceUID, SOPInstanceUID). Персональные данные не используются: + решение не зависит от их наличия.
  2. +
  3. Предобработка. Приводится к виду, ожидаемому предобученным backbone (описание — разд. 5). + Параметры предобработки хранятся в чекпоинте и берутся из него, а не из кода по умолчанию.
  4. +
  5. Оценка качества. Один прямой проход сети даёт логит. Решение принимается по логиту: + quality_class = 1, если логит выше порога. Порог подобран по F1 на валидации и + сохранён в чекпоинте (текущее значение −0.4930, что соответствует вероятности 0.379).
  6. +
  7. Анатомическая область. Определяется независимо от модели, по геометрии кадра (разд. 7), + с оценкой уверенности.
  8. +
  9. Измерения и тип нарушения. Для снимков с нарушением вычисляются числовые признаки снимка, + по которым нарушение относится к категории словаря (разд. 8).
  10. +
  11. Отчёт. Строка собирается со статусом обработки и временем; ошибка на файле не прерывает пакет.
  12. +
+ +
В README.md приведён полный набор проектных диаграмм (компоненты, потоки данных, +последовательность, развёртывание). Они описывают целевую архитектуру, включая блоки, которые в текущей +реализации не используются, поэтому в настоящем документе поток описан текстом по коду.
+ +

4. Состав данных и разметка

+ +

4.1. Набор

+ + + + + + + + + +
ПоказательЗначениеПояснение
Файлов на диске544в обучающем наборе (dataset_hack/НД_для_обучения)
Уникальных снимков252по пиксельному содержимому; 292 файла — побайтные дубли
Исследований100разбиение выполняется по исследованиям, а не по снимкам
Позвоночник / бедро правое / бедро левое / не определено99 / 79 / 73 / 1голосование по именам файлов после склейки дублей
Нарушений по экспертной таблице74 (29.4 %)три снимка таблица не оценивала
Нарушений в рабочей разметке77 (30.6 %)74 по таблице плюс 3 снимка с пометкой в имени файла
+ +

Дубли не пересекают границы исследований, конфликтов меток при склейке не возникает. Два побайтных +дубля названы по-разному, поэтому область определяется голосованием по именам файлов. Изучение +набора выявило две особенности, которые пришлось учесть в разметке: один и тот же снимок встречается +под несколькими именами (без склейки он попадал бы одновременно в оба класса), и имена файлов местами +расходятся с оценкой эксперта.

+ +

4.2. Единица разметки и правило получения метки

+ +

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

+ +

Возможны были два правила: учитывать только экспертную таблицу либо дополнительно учитывать +пометки _good/_bad, проставленные вручную в именах файлов. Пометки +расходились с экспертом в 15 случаях из 252, поэтому правило выбиралось измерением: одно +зафиксированное разбиение, пять seed'ов обучения, один эталон (табл. в разд. 12.3). Выбрано правило +«только экспертная таблица».

+ +

4.3. Разбиение выборки

+ + + + + +
ЧастьСнимковИсследованийНарушенийФайл
Обучение1998161labels/split_expert_seed42.json
Валидация531916
+ +

Разбиение стратифицировано по эталону и фиксировано в файле: все сравниваемые варианты +обучаются и оцениваются на одном и том же held-out наборе, что делает сравнения корректными. +Снимки одного исследования не попадают одновременно в обе части — это исключает утечку, при которой +метрики растут за счёт запоминания конкретных исследований. Наличие утечки и воспроизводимость +разбиения проверяются тестами (tests/test_labels.py).

+ +

5. Предобработка изображений

+ +

Предобработка описана декларативно в src/dxa/preprocess.py (неизменяемая конфигурация +PreprocessConfig) и используется и при обучении, и при работе API. Параметры хранятся в +чекпоинте и восстанавливаются из него, поэтому вход модели не может разойтись между двумя режимами.

+ + + + + + + +
ПараметрЗначениеНазначение
normpercentileприведение интенсивностей к [0, 1]; допустимо также minmax
p_low / p_high0.5 / 99.5отсекаемые перцентили — устойчивость к выбросам и шуму
imagenet_normtrueстандартизация mean (0.485, 0.456, 0.406), std (0.229, 0.224, 0.225), как ожидает предобученный backbone
input_size224сторона квадратного входа сети
+ +

Порядок операций над одним файлом:

+
    +
  1. Чтение. pydicom.dcmread и pixel_array в float32; + многокадровое изображение усредняется по кадрам, иная размерность — ошибка.
  2. +
  3. Коррекция интенсивностей. Учитываются RescaleSlope и + RescaleIntercept; для MONOCHROME1 яркость инвертируется.
  4. +
  5. Нормировка. По перцентилям 0.5 / 99.5 с обрезкой в [0, 1] и защитой от вырожденного + диапазона (hi ≤ lo).
  6. +
  7. Приведение к тензору. Умножение на 255 и uint8, дублирование одного канала + в три, resize 224×224 билинейно, перевод в CHW, деление на 255, затем нормировка ImageNet.
  8. +
+ +

Если в чекпоинте нет блока preprocess (старый файл), применяются значения по умолчанию — +percentile с нормировкой ImageNet; устаревший профиль minmax без ImageNet +доступен флагом --no-imagenet-norm.

+ +
Порядок «resize в uint8, затем нормировка» сохранён намеренно: он совпадает +с историческим кодом проекта, поэтому смена реализации не сдвинула распределение входа и не обесценила +ранее подобранный порог.
+ +

6. Модель и процедура обучения

+ +

6.1. Архитектура

+ + + + + + + + + + + +
ЭлементРеализация
BackboneResNet18 с весами ImageNet (IMAGENET1K_V1), все слои кроме + fc; признак — 512 чисел. Флагом --backbone допустим также + ResNet34
Голова качествапо умолчанию linear: Flatten + Linear(512, 2) — линейный + зонд, 1026 обучаемых параметров. Режим mlp (флаг --head): + Linear(512, 256) + ReLU + Dropout(0.3) + + Linear(256, 2)
Вспомогательная голова областиFlatten + Linear(512, 4): три анатомические области плюс «неизвестно». + Обучается с весом 0.3 как дополнительная задача и заставляет backbone различать анатомию; логиты + качества она не сдвигает
Стандартизация входа головыпризнаки нормируются по среднему и СКО обучающей выборки, зафиксированным в буферах + feat_mean/feat_std. Без неё логиты смещены, вероятности скучены у нуля и + подобранный порог теряет смысл
+ +

6.2. Гиперпараметры и процедура обучения

+ + + + + + + + + + + + + + + + +
ПараметрЗначение по умолчаниюКомментарий
ОптимизаторAdamWlr 3e-4, weight_decay 5e-2. Заметный weight decay нужен не только + против переобучения: без него логиты за 100 эпох насыщаются и порог вырождается
ПланировщикReduceLROnPlateauмножитель 0.5, терпение 3, следит за val_loss
Функция потерьCrossEntropyбалансировка классов выключена (--balance none); доступны loss + (вес n_good / n_bad) и sampler
Батч / эпохи16 / 100ранняя остановка после 25 эпох без улучшения
Отбор чекпоинтаокно 5оценка эпохи — ROC-AUC, сглаженный по последним 5 эпохам; до заполнения окна чекпоинт не + сохраняется, при равном AUC решает F1
Вес головы области0.3--region-loss-weight; снимки с неизвестной областью в её loss не входят
Обрезка градиентовнорма 5.0защита от выбросов
Заморозка backboneвсегда--freeze-epochs -1 по умолчанию (линейный зонд); при размораживании lr падает + в 10 раз
Seed / устройство42 / автопорядок выбора устройства: CUDA → MPS → CPU
+ +

Почему линейный зонд. При ~250 уникальных снимках полный fine-tune ResNet18 за несколько эпох +запоминает обучающую выборку: train F1 стремится к 1.0, а val AUC падает к 0.5. Поэтому по умолчанию +backbone заморожен и обучается только голова на признаках ImageNet. При заморозке слои BatchNorm +остаются в режиме eval: иначе бегущие статистики продолжали бы обновляться и признаки +«уезжали» бы от тех, на которых рассчитывалась стандартизация.

+ +

Почему порог по логиту. Порог хранится и применяется в логитах (log-odds): после обучения +линейный зонд разделяет обучающую выборку почти идеально, вероятности насыщаются в 0/1, и порог в +единицах вероятности вырождается. Для человека порог переводится в вероятность через сигмоиду. ROC-AUC +и PR-AUC считаются по вероятностям и от порога не зависят.

+ +

Как подбирается порог. На каждой эпохе по валидации: кандидаты — все наблюдаемые логиты плюс +края диапазона, выбирается максимум F1; при равном F1 берётся меньший логит, чтобы не терять recall. +Аргумент --min-recall позволяет вместо этого взять максимальный порог с recall не ниже +заданного.

+ +

Рабочий чекпоинт. Лучшая эпоха — 39 из прогона в 64 эпохи (обучение остановлено по терпению), +порог логита −0.4930 (вероятность 0.379). Метрики этой эпохи — в разделе 12, честная оценка варианта на +пяти seed'ах — ROC-AUC 0.6764.

+ +

6.3. Чекпоинт и отчёты

+ +

Чекпоинт самодостаточен: кроме весов в нём лежат архитектура, параметры предобработки, порог и +происхождение данных, поэтому инференс не может рассинхронизироваться с обучением.

+ + + + + + + + + + +
ПолеСодержимое
model_state_dictвеса сети; format_version — 2
backbone, headархитектура; при явном расхождении с запрошенной загрузка завершается ошибкой
preprocessпараметры предобработки (раздел 5)
threshold, threshold_logitрабочий порог по логитам
labels_csv, split_fileна какой разметке и на каком разбиении обучена модель
epoch, val_metrics, selection_scoreэпоха, её метрики и сглаженная оценка, по которой выбран чекпоинт
history, optimizer_state_dictистория обучения и состояние оптимизатора
+ +

Рядом с чекпоинтом обучение пишет train_report.md и train_report.json: +гиперпараметры, метрики на валидации, метрики по областям и историю по эпохам. Артефакты кладутся в +--output-dir (по умолчанию models).

+ +

7. Определение анатомической области

+ +

Область определяется по содержимому снимка, а не по имени файла: на закрытом наборе соглашение об +именах может отсутствовать. Решение принимается по геометрии кадра (resolve_region в +src/dxa/inference.py) независимо от модели качества; обученная вспомогательная голова +области привлекается только как уточнение.

+ +

Признаки считает функция heuristic_signals по маске яркой (костной) ткани с порогом по +95-му перцентилю:

+ + + + + + + +
ПризнакЧто измеряет
widthширина кадра в пикселях — основной признак области на этом оборудовании (у позвоночника 300 px, у бедра 280 px)
bbox_aspectотношение высоты яркой области к её ширине (вытянутость)
left_right_ratioперевес светимости левой половины над правой — наклон в сторону бедра
symmetryсимметрия изображения относительно вертикальной оси
+ +

Порог ширины задан константой SPINE_MIN_WIDTH = 295. Порядок решений:

+
    +
  1. если ширина кадра ≥ 295 — spine, уверенность 0.8;
  2. +
  3. иначе (бедро) — сторона по перевесу светимости: left_right_ratio > 1.3 даёт + hip_right, < 0.7 — hip_left, уверенность 0.6;
  4. +
  5. при промежуточном перевесе — предсказание головы области, если её уверенность ≥ 0.5;
  6. +
  7. если и оно неуверенно — общая область hip с уверенностью 0.4.
  8. +
+ +

Если ширина кадра неизвестна, используется предсказание головы, а затем форма яркой области +(bbox_aspect и symmetry).

+ +

Точность. Сопоставление с областью из рабочей разметки (252 снимка, раздел 4):

+ + + + + + +
Что проверялосьРезультат
Позвоночник99 / 99
Сторона бедра (правое / левое)135 / 152 (88.8 %)
Итого по всем областям234 / 251 (93.2 %)
+ +

Различение позвоночника и бедра по ширине кадра работает безошибочно, а сторона бедра определяется +менее надёжно: перевес светимости путает левое и правое в 17 случаях из 152. Сторона не проверялась по +тегам DICOM — Laterality в наборе пуст, — поэтому это ограничение (раздел 17, п. 7), а не +измеренная ошибка модели.

+ +
Признак «ширина кадра» привязан к текущему аппарату. Он безошибочен на этом наборе, но +при смене оборудования порог потребует калибровки — раздел 17, ограничение 8.
+ +

8. Таксономия нарушений качества

+ +

Коды типов нарушений собраны в одном модуле src/dxa/violations.py. Раньше словарь был +свой в трёх местах (инференс, main.py и веб-интерфейс), и ни одна копия не знала кодов +экспертной таблицы; теперь коды, русские подписи и пояснения заданы один раз, а сервер отдаёт их +клиенту.

+ + + + + + + + + + + + +
КодПодписьОбластьИсточник
positioningНекорректная укладкаspineэкспертная таблица
axis_deviationОтклонение осиspineэкспертная таблица
artifactАртефакты и имплантылюбаяэкспертная таблица
rotationРотация / позиционированиеhipэкспертная таблица
roi_incorrectНекорректная область интересалюбаяэкспертная таблица
motionДвижение, размытиелюбаяcondition_doctor.txt
incomplete_anatomyАнатомия видна не полностьюлюбаяcondition_doctor.txt
labeling_errorОшибка разметкиspinecondition_doctor.txt
unspecifiedНарушение без уточнениялюбаясистемный код
+ +

У каждого кода есть пояснение, чем нарушение мешает измерению (например, для rotation — +что малый вертел виден слишком хорошо и шейка бедра кажется укороченной). Функция +catalogue() отдаёт словарь через /api/v1/model, поэтому подписи в интерфейсе и +в выгрузке не могут разойтись с кодами.

+ +

Для текстового отчёта DICOM SR у каждого кода заданы условный числовой код и английская формулировка. +Полноценного справочника SNOMED/DICOM для контроля качества DXA в наборе нет, поэтому коды условные и +всегда идут рядом с текстом: пригодный снимок кодируется 113001 (DXA image quality +acceptable) с флагом FINAL, нарушение — с флагом WARNING. Устаревшие значения +внешних источников (artifact_motion, roi_error, position_error, +quality_violation_detected и другие) приводит к канону функция canon_type.

+ +

8.1. Тип нарушения в разметке

+ +

Экспертная таблица кодирует не все девять кодов, а пять: positioning, +axis_deviation, artifact, rotation, roi_incorrect. +Распределение в рабочей разметке (в четырёх из 77 нарушений указано по два критерия, поэтому сумма +больше 77):

+ + + + + + + + + +
КодСнимков
rotation36
artifact17
axis_deviation10
roi_incorrect7
positioning6
unspecified5
+ +

Коды motion, incomplete_anatomy и labeling_error в разметке не +встречаются: таблица их не кодирует, хотя словарь их знает ради критериев +condition_doctor.txt. Пять снимков имеют только unspecified, потому что +источник не указывает критерий.

+ +

8.2. Тип нарушения при инференсе

+ +

Модель решает только бинарную задачу, поэтому тип для снимка с нарушением выбирает функция +classify_violation_type по дешёвым признакам: размытие +(laplacian_variance), доля плотных пикселей (bright_fraction) и вытянутость +яркой области (bbox_aspect для бедра). При отсутствии выраженного признака возвращается +unspecified.

+ +
На текущем чекпоинте эта ветка практически вырождена. Пороги +motion_threshold и artifact_threshold в вызов не передаются, поэтому +действуют значения по умолчанию (0.0 и 1.0), которых признаки достичь не могут, а условие ротации +(bbox_aspect вне диапазона 0.4–3.0) на наборе не срабатывает. Прогон по всем 548 файлам +даёт одинаковый результат: все 341 решение с нарушением помечены unspecified. Для +содержательного типа нужна разметка типов на уровне снимка и обученный мультилейбл-классификатор +(раздел 18), поэтому в ответе API тип всегда идёт с флагом +violation_type_is_heuristic = true.
+ +

9. Форматы входных и выходных данных

+ +

9.1. Вход

+ +

Один файл DICOM (для API — один HTTP-загрузкой, для CLI — файл или каталог с рекурсивным обходом). +Исследование может содержать несколько снимков — каждый обрабатывается отдельно и даёт отдельную +строку отчёта. Перед обработкой файл читается целиком; при ошибке чтения строка получает статус +сбоя, а пакет продолжается.

+ +

9.2. Выходной файл

+ +

Состав столбцов проверен на реальных прогонах — и через CLI, и через /api/v1/export. +Обязательные восемь столбцов задания совпадают в обоих путях по составу и порядку; дополнительно +пишутся confidence и violation_reason, а CLI-инференс добавляет одиннадцатый +столбец region_confidence. Таблица ниже отражает CLI-вывод; в ответе +/api/v1/export последнего столбца нет.

+ + + + + + + + + + + + + + +
№СтолбецТипСодержимое
1path_to_studyстрокапуть к исследованию (для загрузки через API — upload://имя)
2study_uidстрокаStudyInstanceUID из DICOM
3image_uidстрокаSOPInstanceUID из DICOM
4anatomical_regionстрокаspine / hip_left / hip_right / hip
5quality_classцелое0 — качественное, 1 — есть нарушение
6violation_typeстрокакод нарушения или пусто
7processing_statusстрокаSuccess либо Failure: <причина>
8time_of_processingвещественноевремя обработки снимка, секунды
9confidenceвещественноеуверенность модели в решении
10violation_reasonстрокатекстовое пояснение к отнесённой категории
11region_confidenceвещественноеуверенность определения области
+ +
Время обработки отдаётся в столбце файла результатов; одиночный ответ +POST /api/v1/analyze этого столбца не содержит — при интеграции время измеряется на +стороне вызывающей системы (см. разд. 10 и 13).
+ +
Расхождение ровно в один столбец — единственное различие двух путей выгрузки; оно +перечислено в разделе 16. Обязательные столбцы и их порядок при этом одинаковы, поэтому автоматический +разбор результатов не зависит от того, каким путём получен файл.
+ +

10. API сервиса

+ +

Сервис на FastAPI. Маршруты и состав полей ответов ниже приведены по фактическим ответам на +запросах к тестовым файлам, а не по схеме из документации кода.

+ + + + + + + + + + + +
МетодПутьНазначение
GET/веб-интерфейс
GET/api/v1/healthстатус сервиса; поля ответа: status, device, model_loaded, model
GET/api/v1/modelкарточка решения: model, labels, dataset, comparison, violations, limitations, sources, snapshot_date, loaded
POST/api/v1/analyzeанализ одного файла (multipart-загрузка)
POST/api/v1/analyze/detailedрасширенный отчёт, опционально с маской
POST/api/v1/analyze/srтекстовое представление отчёта DICOM SR: format, sr_content, UID
POST/api/v1/batchпакетная обработка нескольких файлов
POST/api/v1/exportпакетная обработка и выгрузка в XLSX
+ +

10.1. Состав ответа /api/v1/analyze

+ +

Поля: filename, study_uid, image_uid, +anatomical_region, region_confidence, quality_class, +quality_label, overall_quality, severity, +confidence, confidence_per_class, threshold_probability, +violation_type, violation_type_label, +violation_type_is_heuristic, violation_type_note, reason, +metrics, samples, processing_status.

+ +

Два поля заслуживают пояснения при интеграции:

+ + +

10.2. Расширенный ответ /api/v1/analyze/detailed

+ +

Дополнительно к предыдущему набору: image (визуализация снимка), mask +(маска зоны измерения, при запросе), reasons, metrics_note, +view_quality, а также измерения по областям: spine_completeness, +hip_completeness, hip_rotation.

+ +
Измерения в расширенном ответе сопровождаются пометкой о том, что их пороги не +калиброваны (разд. 17), и не превращаются в вердикты «да/нет»: интерфейс показывает числа.
+ +

11. Веб-интерфейс

+ +

Интерфейс предназначен для ручной проверки работы сервиса и для демонстрации: загрузка файлов, +таблица результатов со статистикой, панель деталей по выбранной строке и панель «О модели» с +источником загруженного чекпоинта.

+ +
+ Таблица результатов: полоса состояния, статистика, строки снимков +
Рис. 1. Результаты обработки: полоса состояния показывает, какой чекпоинт отвечает + (разметка, эпоха, порог), рядом — статистика и таблица снимков.
+
+ +
+
+ Панель деталей: снимок с нарушением +
Рис. 2а. Нарушение: уровень, заключение с вероятностью и порогом, тип нарушения с пометкой «эвристика».
+
+
+ Панель деталей: качественный снимок +
Рис. 2б. Качественный снимок: измерения подписаны как справочные.
+
+
+ +

Отдельный принцип — интерфейс не утверждает больше, чем известно решению:

+ + +
+ Панель «О модели»: чекпоинт, метрики с интервалами, данные, словарь нарушений +
Рис. 3. Панель «О модели»: источник чекпоинта, метрики с доверительными интервалами, + состав данных, словарь нарушений и список ограничений. Данные приходят из /api/v1/model.
+
+ +

12. Метрики качества

+ +

12.1. Основная оценка

+ +

Оценка получена на фиксированном разбиении по исследованиям (обучение 199 снимков / 81 исследование, +валидация 53 снимка / 19 исследований, 16 нарушений), пять seed'ов обучения, эталон — вердикт +эксперта из таблицы. Приоритетные по заданию метрики приведены с 95 % доверительными интервалами.

+ + + + + + + +
МетрикаЗначение95 % ДИКомментарий
ROC-AUC0.6764[0.6309, 0.7218]приоритетная метрика задания
PR-AUC0.4759[0.4141, 0.5377]базовый уровень при доле нарушений 30 % — около 0.30
F10.5676[0.5270, 0.6082]порог подбирался по F1 на той же валидации, поэтому значение смещено вверх
Recall / Precision0.700 / 0.486—рабочая точка выбранного порога
+ +

12.2. Оценка по областям и контрольные проверки

+ + + + + + +
Что измереноЗначениеКак получено
ROC-AUC: позвоночник / бедро правое / бедро левое0.943 / 0.576 / 0.550рабочий чекпоинт, собственная валидация
Контрольная задача «позвоночник / бедро»AUC 1.00проверка работоспособности пайплайна, а не клиническая метрика
Модель против правила «позвоночник = нарушение»0.854 против 0.529оценка на всём наборе, включая обучающие снимки, поэтому смещена вверх
+ +

Разбивка по областям нужна потому, что нарушения распределены неравномерно: в позвоночнике +33 из 99 снимков, у бёдер 21–22 из 73–79, а сама область почти однозначно определяется по ширине +кадра. Поэтому общий AUC частично отражает различение области, а не только распознавание дефекта. +Чтобы отделить одно от другого, выполнена проверка: правило «позвоночник = нарушение» даёт внутри +областей 0.50 (подсказки нет), модель — 0.85–0.93. Это означает, что модель использует содержимое +снимка, а не только область.

+ +

12.3. Выбор правила разметки

+ + + + + + + +
Метрика (эталон)только таблицас суффиксами имён
ROC-AUC0.6764 [0.6309, 0.7218]0.6199 [0.5840, 0.6559]
PR-AUC0.4759 [0.4141, 0.5377]0.4046 [0.3702, 0.4391]
F10.5676 [0.5270, 0.6082]0.5426 [0.5073, 0.5778]
Recall / Precision0.700 / 0.4860.863 / 0.404
+ +

Парная разница (только таблица минус с суффиксами): ROC-AUC +0.0564 [+0.0403, +0.0725], +PR-AUC +0.0713 [+0.0398, +0.1028] — знаки +++++, то есть преимущество на всех пяти +seed'ах. Принято правило «только экспертная таблица». Оговорка, важная для интерпретации: эталон — та +же таблица, поэтому вариант, обучавшийся на ней, находится в выигрышном положении; значимо здесь то, +что добавление ненадёжных пометок согласие с экспертом снижает, а не повышает.

+ +

13. Производительность и системные требования

+ +

13.1. Измерения

+ +

Измерения выполнены на рабочей машине разработчика (macOS, Apple Silicon) на полном наборе данных — +548 файлов в dataset_hack (544 обучающих плюс 4 тестовых). Инференс принудительно +переведён на CPU параметром --device cpu, чтобы числа не зависели от наличия ускорителя.

+ + + + + + + + + + +
ПоказательЗначениеУсловия
Время на изображение (медиана)0.021 сCPU, включает чтение DICOM, предобработку и проход сети
Время на изображение (максимум)0.131 стот же прогон; первый снимок включает прогрев
Пакет целиком (548 файлов)12.1 сот запуска процесса до записи XLSX, включая загрузку чекпоинта
Доля успешно обработанных файлов548 / 548 (100 %)ошибок чтения на этом наборе нет
Пиковая память процесса≈ 640 МБmaximum resident set size, пакетный CPU-инференс
Ответ API на один файл16–18 мсвызов внутри процесса, после прогрева; первый запрос 390 мс. Здесь устройство выбрано автоматически (MPS), на CPU значение того же порядка — см. медиану пакетного прогона выше
Время старта сервиса≈ 3 симпорт библиотек и загрузка чекпоинта
+ +

Отсюда следует прямой ответ на требование «не более 3 минут на исследование»: даже если +исследование содержит максимальные по заданию три снимка, обработка занимает доли секунды, а +трёхминутный бюджет расходуется практически только на передачу файлов и накладные расходы +интеграции. Запас более чем трёхкратный, поэтому узким местом решение не ограничивает.

+ +

13.2. Минимальная и рекомендуемая конфигурация

+ + + + + + + + +
РесурсМинимальная конфигурацияРекомендуемая
Процессор2 ядра x86-64 или ARM644 ядра и более; инференс векторизован и масштабируется по кадрам
Оперативная память1.5 ГБ (измеренный пик ≈ 640 МБ + запас на ОС и веб-слой)4 ГБ
Графический ускорительНе требуется: torch устанавливается из CPU-индекса, инференс на CPU укладывается в требование по времениЛюбой GPU с поддержкой CUDA 11.8 — вариант сборки Dockerfile_cuda
Дисковое пространствобазовый образ python:3.11-slim (150 МБ) + зависимости и код; чекпоинт 43 МБ монтируется отдельноплюс место под входные архивы DICOM и результаты
СетьНе требуется при работе: образ и веса фиксированы, обращений во внешние сервисы нетНужна только для сборки образа и загрузки базовых зависимостей
+ +

Размер образа (измерено): 1.41 ГБ (1 412 385 143 байт) для сборки под +linux/arm64. Базовый образ python:3.11-slim занимает 150 МБ, остальное — +зависимости из requirements.txt, включая torch и torchvision из CPU-индекса; CUDA-колёса +в образ не попадают. Данные, тесты, labels/ и assets/ в образ не входят, +чекпоинт (43 МБ) монтируется отдельно.

+ +

14. Сборка, запуск и развёртывание

+ +

14.1. Состав образа

+ + + + + + + + + + + +
ЭлементЗначение
Базовый образpython:3.11-slim (фиксированный тег)
Зависимостиrequirements.txt с точными версиями; torch и torchvision — из CPU-индекса PyTorch, чтобы в образ не попали CUDA-колёса
Что копируется в образтолько src/, requirements.txt и run.sh
Что не копируетсяданные, тесты, labels/, assets/, docs/ (см. .dockerignore), а также чекпоинт
Чекпоинтмонтируется при запуске в /app/models; путь задаётся переменной DXA_MODEL_PATH (по умолчанию /app/models/dxa_model.pth)
Проверки на этапе сборкиимпорт приложения (import src.main) и наличие офлайн-ассетов фронтенда; сборка падает при их отсутствии
Контроль состоянияHEALTHCHECK обращается к /api/v1/health каждые 30 с
Точка входаuvicorn src.main:app на порту 8000
+ +

Если чекпоинт не смонтирован, сервис всё равно поднимается: /api/v1/health сообщает +model_loaded: false, а запросы анализа возвращают ошибку вместо тихой неверной оценки. +Это сознательный выбор: отсутствие модели должно быть заметно сразу, а не проявляться как «странные» +результаты.

+ +

Что проверено на самом образе. Сборка прошла обе внутренние проверки +(app import ok, frontend assets ok). Образ запущен и проверен в двух режимах: +без смонтированного чекпоинта /api/v1/health отдаёт model_loaded: false и +exists: false, а запрос анализа — HTTP 500 с телом {"error": "Model not loaded"}; +с примонтированным каталогом моделей /api/v1/health сообщает model_loaded: true, +device: cpu, эпоху 39 и файл разметки labels/labels_images.csv, запрос анализа +отвечает корректной строкой результата, а /api/v1/export возвращает XLSX с ожидаемым +набором столбцов. Сквозная проверка выполнялась в контейнере на той же машине, где снимались +измерения производительности.

+ +

14.2. Воспроизводимость

+ + + +

14.3. Сборочный конвейер

+ +

Jenkinsfile выполняет три шага: сборка образа, публикация в реестр и выкладка в +кластер. Скрипт run.sh — единая точка входа для разработки и эксплуатации: обучение, +разметка, приведение имён файлов, инференс, сервис, сравнение вариантов разметки и тесты +(перечень команд — приложение Б).

+ +

15. Тесты и проверки

+ +

Тесты — pytest, 209 проверок в шести файлах; запуск — ./run.sh test или +python -m pytest tests/ -q. Сверка выполнена на дату документа: 209 passed.

+ + + + + + + + + + + + + + + +
ФайлТестовЧто проверяет
tests/test_labels.py38разбор имён, определение области и метки по имени, стратифицированное разбиение по + исследованиям, отсутствие утечки, фиксация и переиспользование разбиения в файле, сверка с + реальным датасетом
tests/test_excel_labels.py50разметка по экспертной таблице: чтение критериев, правило «1 = нарушение», голосование по + области, перенос оценки на единственное бедро, три правила метки + (table / union / expert), их согласованность и подключение + к обучению
tests/test_rename_files.py36приведение имён DICOM: разбор и канонизация, поиск свободного номера при конфликте, отказ + угадывать область, цикл «применить → откатить»
tests/test_preprocess_and_model.py32конфигурация и нормировка, сборка тензора и нормировка ImageNet, метрики, подбор порога, + контракт модели, BatchNorm при заморозке, roundtrip чекпоинта, проверка на реальных снимках
tests/test_violations.py28единый словарь типов: приведение устаревших значений, подписи, коды SR и каталог для API, + согласованность с критериями экспертной таблицы
tests/test_api_contract.py25поля ответов, которые читает веб-интерфейс: базовый и детальный анализ, здоровье и карточка + модели, выгрузка XLSX, обработка ошибок; различимость метрик между снимками и валидность + PNG-визуализаций
+ +

Отдельный контур — браузерные проверки интерфейса (tests/browser/*.js, Node и +playwright-core): они открывают интерфейс во временном профиле Chrome, загружают DICOM, +кликают по строкам таблицы и снимают содержимое панели деталей. Требуется запущенный сервер на +127.0.0.1:8123; профиль пользователя не затрагивается.

+ + + + + + + + + +
СценарийЧто проверяет
ui_check.jsподсказку о кликабельности строк и её скрытие при пустом фильтре; открытие панели деталей из + строки; изменение значений при переключении строк; наполнение панели «О модели» метриками и + словарём
ui_violation.jsветку «нарушение»: бейдж, POOR, HIGH, текст заключения
ui_offline.jsотсутствие обращений страницы к внешним хостам
+ +

16. Известные ошибки и их обработка

+ +

Требование задания — отсутствие необработанных исключений: сбой на одном файле фиксируется в отчёте и +не прерывает пакет. Ниже — фактические режимы отказа и способ обработки.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
СитуацияПоведениеГде обрабатывается
Ошибка чтения или обработки файла (CLI)строка получает quality_class = -1, область unknown, пустые UID и + processing_status = Failure: <Тип>: <сообщение> (до 120 символов); + остальные файлы обрабатываются дальшеprocess_one, src/dxa/inference.py
Неподдерживаемая форма pixel_array (не 2D и не 3D)ValueError, превращается в Failure: согласно строке вышеpreprocess.py
Чекпоинт отсутствует или не читаетсясервис поднимается, /api/v1/health отдаёт model_loaded: false, а + запросы анализа — HTTP 500 {"error": "Model not loaded"}load_model(), src/main.py
Архитектура чекпоинта не совпала с запрошеннойValueError с указанием сохранённых backbone / headload_model, src/dxa/inference.py
Чекпоинт — «сырой» state_dict без метаданныхValueError: файл не содержит model_state_dictDXAQualityModel.load
Сбой в одиночном запросе /analyze, /analyze/detailed, + /analyze/srHTTP 500 с телом {"error": <текст>, "processing_status": "Failure: <первые 50 + символов>"}; детальный эндпоинт дополнительно печатает стек в лог сервераобработчики маршрутов
Сбой одного файла в /api/v1/batchэлемент становится {"filename", "error", "processing_status": "Failure"}, + остальные файлы возвращаются нормальноbatch_analyze
Сбой одного файла в /api/v1/exportстрока со статусом Failure: <до 80 символов> и quality_class = -1 + попадает в XLSX, выгрузка не прерываетсяexport_results
Файл разметки не найден при обучениипредупреждение в лог и откат на метки из имён файлов; откат залогирован явно, потому что + вместе с источником меток меняются метрикиresolve_labels_csv, src/dxa/train.py
Пустое разбиение (нет обучающих или валидационных снимков)RuntimeError с числами train / val до начала обученияtrain()
+ +
Сервис без чекпоинта намеренно не пытается «угадывать»: он поднимается, но честно +сообщает model_loaded: false и отклоняет запросы анализа. Молчаливая неверная оценка вместо +явной ошибки была бы опаснее в медицинском контуре.
+ +

17. Ограничения и достоверность результатов

+ +

Ограничения сформулированы так, чтобы читатель мог оценить границы применимости решения, а не +только увидеть итоговые метрики.

+ +
    +
  1. Разметка выведена из оценки исследования, а не снимка. Экспертная таблица описывает + исследование; перенос вердикта на снимок однозначен (область встречается один раз), но + поштучной экспертной оценки снимков в наборе нет. Оценка качества модели упирается в качество + этой разметки, а не только в объём данных.
  2. +
  3. Мало данных. 252 уникальных снимка, 77 нарушений. Доверительные интервалы широкие + (ROC-AUC 0.6764 [0.6309, 0.7218] по пяти seed'ам), поэтому оценка на закрытом наборе может + отличаться.
  4. +
  5. Эталон — та же таблица. Независимой истины нет, поэтому сравнение правил разметки + частично благоприятствует варианту «только таблица» (раздел 12.3).
  6. +
  7. В DICOM нет разметки областей интереса. Ни overlay, ни graphic annotation в файлах нет, + поэтому корректность нанесённых областей измерения нельзя проверить прямым сравнением с + эталоном: оценивается геометрия видимой зоны.
  8. +
  9. Тип нарушения определяется признаками снимка, а не обученной моделью. Модель решает только + бинарную задачу, поэтому в выгрузке тип содержателен только для размеченных снимков, а на + инференсе эвристика на текущих данных всегда даёт «не уточнён» (раздел 8.2). Для честного + мультикласса нужна разметка типов на уровне снимка; пять снимков имеют только код + «не уточнён», потому что источник не указывает критерий.
  10. +
  11. Три снимка размечены по пометке в имени файла — экспертная таблица их область не + оценивала. Такие строки помечены признаком filename_fallback.
  12. +
  13. Сторона бедра в семи исследованиях с единственным снимком не проверяема: теги + Laterality пусты, оценка взята из единственного заполненного столбца таблицы.
  14. +
  15. Порог определения области привязан к текущему оборудованию. Признак «ширина кадра» + безошибочно работает на этом наборе, но при смене аппарата потребует калибровки.
  16. +
  17. Числовые эвристики панели деталей не калиброваны. Их пороги рассчитаны на другой + масштаб интенсивностей, поэтому в интерфейсе показываются измерения, а не вердикты.
  18. +
  19. Рабочая точка порога даёт высокий recall при умеренной точности. На обучающем наборе при + пороге, подобранном по F1, модель относит к нарушениям 341 строку из 548 (≈ 62 %) при + фактической доле нарушений 30.6 %, что согласуется с precision 0.486. Порог выбран в пользу + полноты: пропустить непригодное исследование дороже, чем показать лишнее.
  20. +
+ +

18. План развития

+ + + +

Приложение А. Структура репозитория

+ +
bone_2026/
+├── src/
+│   ├── main.py                     FastAPI: маршруты и загрузка модели
+│   ├── dxa/                        действующий модуль оценки качества
+│   │   ├── preprocess.py           DICOM -> тензор (общий путь для обучения и API)
+│   │   ├── model.py                сеть, метрики, подбор порога, чекпоинт
+│   │   ├── train.py                обучение и отчёт (md/json)
+│   │   ├── dataset.py              DXADataset, DataLoader
+│   │   ├── inference.py            пакетный инференс, определение области, визуализация
+│   │   ├── labels.py               разбор имён, метки, склейка дублей, разбиение
+│   │   ├── excel_labels.py         разметка снимков по экспертной таблице
+│   │   ├── rename_files.py         приведение имён DICOM к единому виду
+│   │   ├── violations.py           единый словарь типов нарушений
+│   │   ├── model_card.py           карточка решения для API и интерфейса
+│   │   ├── compare_labels.py       сравнение источников разметки
+│   │   ├── discriminator.py        проверка вклада содержимого снимка
+│   │   └── render.py               рендер снимков и контактных листов
+│   ├── quality/                    эвристики: измерения снимка
+│   ├── api/static/                 веб-интерфейс (офлайн-ассеты)
+│   └── utils/                      выбор устройства
+├── labels/                         размеченные данные, разбиение, карта переименований
+├── assets/                         схемы, скриншоты, обоснование разметки
+├── models/dxa_model.pth            рабочий чекпоинт и отчёт обучения
+├── tests/                          pytest + браузерные проверки интерфейса
+├── docs/                           настоящий документ (HTML и PDF)
+├── run.sh, Dockerfile, Dockerfile_cuda, Jenkinsfile, requirements.txt
+ +

Приложение Б. Команды

+ +
# обучение и разметка
+./run.sh label                          разметить датасет по экспертной таблице
+./run.sh rename                         план приведения имён файлов (--apply для применения)
+./run.sh train                          обучить модель (режим меток по умолчанию)
+./run.sh split && ./run.sh compare       сравнить варианты разметки на одном разбиении
+
+# инференс и сервис
+./run.sh infer "dataset_hack/Для теста" results.xlsx
+./run.sh serve                          API и веб-интерфейс на порту 8000
+python -m src.dxa.discriminator         проверка вклада содержимого снимка
+
+# проверки
+./run.sh test                           pytest (209 тестов)
+node tests/browser/ui_check.js          проверки интерфейса (нужен сервер на 127.0.0.1:8123)
+
+# контейнер
+docker build -t dxa-quality .
+docker run -v /path/to/data:/data -v /path/to/models:/app/models -p 8000:8000 dxa-quality
+ +

Документ подготовлен по фактическому состоянию репозитория; машинные отчёты обучения — +models/train_report.json, обоснование разметки — assets/labeling.md.

+ + + diff --git a/docs/technical-description.pdf b/docs/technical-description.pdf new file mode 100644 index 0000000..280bee3 Binary files /dev/null and b/docs/technical-description.pdf differ