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

Техническое описание решения
ЗадачаКонтроль качества DXA-исследований: бинарная оценка «качественное / есть нарушение», определение анатомической области, тип нарушения, структурированный отчёт
Дата документа27 сентября 2026 г.
КомандаГрачев Денис — разработка; Грачев Татьяна — капитан
Версия решения1.0.0
Рабочий чекпоинтmodels/dxa_model.pth: ResNet18 + линейная голова, разметка table, seed 42, эпоха 57, порог логита −0.1370 (вероятность 0.466)
Ключевые метрикиROC-AUC 0.6726 [0.6367, 0.7086], F1 0.5559 [0.5212, 0.5906] — пять seed'ов на фиксированном разбиении по исследованиям
Время обработки0.022 с на изображение (медиана, CPU), 482 файла за 13.9 с; требование «≤ 3 мин на исследование» выполняется с запасом

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

Содержание

  1. 1. Назначение и область применения
  2. 2. Соответствие требованиям задания
  3. 3. Архитектура решения
  4. 4. Состав данных и разметка
  5. 5. Предобработка изображений
  6. 6. Модель и процедура обучения
  7. 7. Определение анатомической области
  8. 8. Таксономия нарушений качества
  9. 9. Форматы входных и выходных данных
  10. 10. API сервиса
  11. 11. Веб-интерфейс
  12. 12. Метрики качества
  13. 13. Производительность и системные требования
  14. 14. Сборка, запуск и развёртывание
  15. 15. Тесты и проверки
  16. 16. Известные ошибки и их обработка
  17. 17. Ограничения и достоверность результатов
  18. 18. План развития
  19. Приложение А. Структура репозитория
  20. Приложение Б. Команды

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.5 CLI-инференс и POST /api/v1/export; обязательные восемь столбцов плюс три диагностических Разд. 9 (состав столбцов проверен на реальном прогоне)
Время обработки одного исследования ≤ 3 мин Один снимок — один прямой проход сети 224×224, без итеративных процедур Разд. 13: 0.022 с медиана на 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 и маршруты ручной разметки /api/v1/labeling/* Разд. 10
Дополнительно (п. 2.6): коррекция разметки с возможностью подтверждения специалистом Интерфейс /label: снимок целиком, текущая метка с указанием источника, вердикт специалиста сохраняется в labels/manual_labels.csv и принимается обучением как --labels-csv Разд. 11.1; tests/test_manual_labels.py, tests/test_labeling_api.py, tests/browser/ui_labeling.js
Обязательная контейнеризация, скрипт сборки и запуска в Linux Dockerfile (python:3.11-slim, чекпоинт внутри образа), Dockerfile_cuda для GPU, docker-compose.yml на оба случая, run.sh Разд. 14; сборка и запуск проверены, см. 14
Фиксация зависимостей, включая базовый контейнер requirements.txt с точными версиями, базовый образ по тегу, torch с CPU-индексом (CPU-вариант) и cu126 (GPU-вариант) Разд. 14
Работа без обращения к внешним сервисам Офлайн-ассеты фронтенда, отсутствие сетевых вызовов в коде предсказания tests/browser/ui_offline.js
Полный комплект документации README.md (обзор и воспроизведение), assets/labeling.md (разметка и её обоснование), docs/technical-description.html и .pdf (данный документ) —

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

3.1. Слои

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

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

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

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

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

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

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

4.1. Набор

ПоказательЗначениеПояснение
Файлов на диске478DICOM в обучающем наборе (dataset_hack/НД_для_обучения); лишние побайтные копии удалены
Уникальных снимков251по пиксельному содержимому; 227 файлов — дубли одного и того же кадра
Исследований100разбиение выполняется по исследованиям, а не по снимкам
Позвоночник / бедро правое / бедро левое / не определено99 / 78 / 73 / 1голосование по именам файлов после склейки дублей
Нарушений по экспертной таблице73 (29.1 %)три снимка таблица не оценивала
Нарушений в рабочей разметке76 (30.3 %)73 по таблице плюс 3 снимка с пометкой в имени файла

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

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

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

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

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

ЧастьСнимковИсследованийНарушенийФайл
Обучение1988160labels/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. Коррекция интенсивностей. Учитываются RescaleSlope и RescaleIntercept; для MONOCHROME1 яркость инвертируется.
  3. Нормировка. По перцентилям 0.5 / 99.5 с обрезкой в [0, 1] и защитой от вырожденного диапазона (hi ≤ lo).
  4. Приведение к тензору. Умножение на 255 и uint8, дублирование одного канала в три, resize 224×224 билинейно, перевод в CHW, деление на 255, затем нормировка ImageNet.

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

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

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

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

ЭлементРеализация
Backbone ResNet18 с весами 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. Гиперпараметры и процедура обучения

ПараметрЗначение по умолчаниюКомментарий
ОптимизаторAdamW lr 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 не ниже заданного.

Рабочий чекпоинт. Лучшая эпоха — 57 из прогона в 82 эпохи (обучение остановлено по терпению), порог логита −0.1370 (вероятность 0.466). Метрики этой эпохи — в разделе 12, честная оценка варианта на пяти seed'ах — ROC-AUC 0.6726.

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. иначе (бедро) — сторона по перевесу светимости: left_right_ratio > 1.3 даёт hip_right, < 0.7 — hip_left, уверенность 0.6;
  3. при промежуточном перевесе — предсказание головы области, если её уверенность ≥ 0.5;
  4. если и оно неуверенно — общая область hip с уверенностью 0.4.

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

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

Что проверялосьРезультат
Позвоночник99 / 99
Сторона бедра (правое / левое)134 / 151 (88.7 %)
Итого по всем областям233 / 250 (93.2 %)

Различение позвоночника и бедра по ширине кадра работает безошибочно, а сторона бедра определяется менее надёжно: перевес светимости путает левое и правое в 17 случаях из 151. Сторона не проверялась по тегам 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. Распределение в рабочей разметке (в четырёх из 76 нарушений указано по два критерия, поэтому сумма больше 76):

КодСнимков
rotation35
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) на наборе не срабатывает. Прогон по всем 482 файлам даёт одинаковый результат: все 206 решений с нарушением помечены 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.

11.1. Ручная разметка (/label)

Поштучной экспертной оценки снимков в наборе нет — это ограничение 1 в разд. 17, и автоматически его закрыть нечем: локальная vision-модель оказалась непригодна как разметчик (отрицательный результат описан в assets/labeling.md §11). Поэтому реализован отдельный интерфейс ручной разметки, который отвечает пункту 2.6 задания — «автоматическая коррекция разметки с возможностью подтверждения специалистом».

Страница доступна по адресу /label и работает на тех же офлайн-ассетах, что и основная. Она показывает снимок целиком (PNG через /api/v1/labeling/image, без наложений), его текущую метку и источник этой метки: table (экспертная таблица), filename (пометка в имени файла) или manual (вердикт специалиста). Снимки, где метка разметки расходится с пометкой в имени файла, выводятся первыми: именно там один из источников ошибается, и таких снимков в наборе 62 из 251.

Специалист подтверждает вердикт или ставит свой: анатомическая область, «годное / нарушение», тип нарушения из словаря (разд. 8) и комментарий. Вердикт проверяется на согласованность: тип нарушения обязан относиться к выбранной области (ротация — критерий бедра), у качественного снимка типа быть не может, а неизвестный код отклоняется, а не подменяется на «не уточнён». Есть горячие клавиши (g — годное, b — нарушение, Enter — сохранить и перейти к следующему) и прогресс по областям, чтобы работу можно было вести частями.

Вердикты сохраняются в labels/manual_labels.csv в том же формате, что и построенная разметка, поэтому файл читается тем же загрузчиком и принимается обучением как --labels-csv. Выгрузка scope=all отдаёт весь набор с наложенными ручными вердиктами (готовый источник меток), scope=reviewed — только разобранные снимки, чтобы сверить их с построенной разметкой.

Разметка без сервиса. Интерфейс /label требует запущенного сервиса, а размечать должен специалист — как правило, на своей машине, вне сети и без установки чего-либо. Поэтому тот же сценарий упаковывается в один автономный HTML-файл (src/dxa/review_pack.py, около 7 МБ на 251 снимок): изображения встроены как data-URI, вердикты хранятся в браузере, а кнопка «Выгрузить CSV» отдаёт файл, который принимается обучением как есть. Страница не делает ни одного сетевого запроса, и это проверяется автоматически (tests/browser/review_pack.js). Вердикты специалиста накладываются на построенную разметку командой python -m src.dxa.review_pack --merge; если в файле окажутся пути из другого набора, команда сообщает об этом.

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

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

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

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

МетрикаЗначение95 % ДИКомментарий
ROC-AUC0.6726[0.6367, 0.7086]приоритетная метрика задания
PR-AUC0.4753[0.4188, 0.5318]базовый уровень при доле нарушений 30 % — около 0.30
F10.5559[0.5212, 0.5906]порог подбирался по F1 на той же валидации, поэтому значение смещено вверх
Recall / Precision0.713 / 0.472—рабочая точка выбранного порога

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

Что измереноЗначениеКак получено
ROC-AUC: позвоночник / бедро правое / бедро левое0.929 / 0.606 / 0.567рабочий чекпоинт, собственная валидация
Контрольная задача «позвоночник / бедро»AUC 1.00проверка работоспособности пайплайна, а не клиническая метрика
Модель против правила «позвоночник = нарушение»0.885 против 0.534оценка на всём наборе, включая обучающие снимки, поэтому смещена вверх

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

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

Метрика (эталон)только таблицас суффиксами имён
ROC-AUC0.6726 [0.6367, 0.7086]0.6003 [0.5592, 0.6415]
PR-AUC0.4753 [0.4188, 0.5318]0.3864 [0.3487, 0.4242]
F10.5559 [0.5212, 0.5906]0.5354 [0.4978, 0.5729]
Recall / Precision0.713 / 0.4720.788 / 0.409

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

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

13.1. Измерения

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

ПоказательЗначениеУсловия
Время на изображение (медиана)0.022 сCPU, включает чтение DICOM, предобработку и проход сети
Время на изображение (максимум)0.041 стот же прогон
Пакет целиком (482 файла)13.9 сот запуска процесса до записи XLSX, включая загрузку чекпоинта
Доля успешно обработанных файлов482 / 482 (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 12.6, включая H200 (compute capability 9.0) — вариант сборки Dockerfile_cuda
Дисковое пространствобазовый образ python:3.11-slim (150 МБ) + зависимости, код и чекпоинт (43 МБ) — всё внутри образаплюс место под входные архивы DICOM и результаты
СетьНе требуется при работе: образ и веса фиксированы, обращений во внешние сервисы нетНужна только для сборки образа и загрузки базовых зависимостей

Размер образа (измерено). CPU-вариант: 1.46 ГБ под linux/arm64 и 1.8 ГБ под linux/amd64. GPU-вариант: 7.0 ГБ под linux/amd64 — почти всё сверх базы занимают CUDA-библиотеки внутри колёс torch (nvidia-cublas-cu12, nvidia-cudnn-cu12, nvidia-nccl-cu12 и другие). Базовый образ python:3.11-slim — 150 МБ; torch и torchvision берутся из CPU-индекса в CPU-варианте и из индекса cu126 в GPU-варианте, поэтому CUDA-колёса в CPU-образ не попадают (там torch 2.8.0+cpu и ноль nvidia-пакетов). Данные, тесты, labels/, assets/ и docs/ в образ не входят; чекпоинт (43 МБ), наоборот, лежит внутри — монтировать каталог с моделями при запуске не нужно.

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

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

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

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

Что проверено на самом образе. Сборка проходит три внутренние проверки: app import ok, frontend assets ok и checkpoint ok: resnet18 linear threshold -0.13703127205371857. Контейнер запущен без единого монтирования (docker run -d -p 8000:8000 dxa-quality:cpu): чекпоинт присутствует внутри образа (/app/models/dxa_model.pth), /api/v1/health сообщает model_loaded: true, device: cpu, эпоху 57 и файл разметки labels/labels_images.csv, запрос /api/v1/analyze отвечает корректной строкой результата, а /api/v1/export возвращает XLSX с ожидаемым набором столбцов. Сквозная проверка выполнена в контейнере на той же машине, где снимались измерения производительности. Оба варианта дополнительно собраны под целевой linux/amd64 (эмуляция на той же машине): CPU-образ содержит torch 2.8.0+cpu и ни одного nvidia-пакета, GPU-образ — torch 2.8.0+cu126 с cuDNN 9.10.2 и библиотекой libtorch_cuda.so; в обоих сервис стартует, и /api/v1/health, /api/v1/analyze и /api/v1/export отвечают корректно (device: cpu, поскольку ускорителя в машине нет). Работа на самом GPU не проверялась: для неё нужна машина с драйвером NVIDIA, где критерий приёмки — torch.cuda.is_available() == True и device: cuda в /api/v1/health.

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

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

Jenkinsfile выполняет три шага: сборка образа, публикация в реестр и выкладка в кластер. Скрипт run.sh — единая точка входа для разработки и эксплуатации: обучение, разметка, приведение имён файлов, инференс, сервис, сравнение вариантов разметки и тесты (перечень команд — приложение Б). Для запуска образа есть docker-compose.yml с двумя сервисами (14.1). Файл выкладки swarm (/data/deploy/rell-bone-2026/docker-swarm.yml) лежит вне репозитория: если он монтирует каталог с моделями в /app/models, монтирование перекроет встроенный чекпоинт, поэтому его нужно убрать при обновлении выкладки.

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

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

ФайлТестовЧто проверяет
tests/test_labels.py38 разбор имён, определение области и метки по имени, стратифицированное разбиение по исследованиям, отсутствие утечки, фиксация и переиспользование разбиения в файле, сверка с реальным датасетом
tests/test_excel_labels.py51 разметка по экспертной таблице: чтение критериев, правило «1 = нарушение», голосование по области, перенос оценки на единственное бедро, три правила метки (table / union / expert), их согласованность, подключение к обучению и чтение CSV, сохранённого Excel с BOM
tests/test_manual_labels.py26 ручная разметка: проверка вердикта (область, метка, совместимость типа нарушения с областью, отказ от опечаток), хранение и правка вердиктов, чтение файла с BOM, наложение поверх построенной разметки, подсчёт прогресса и выгрузка
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/test_labeling_api.py19 контракт /api/v1/labeling/*: отказ отдавать файлы вне датасета (обход каталога, не-DICOM), проверка вердикта, выгрузка, читаемая обучением как --labels-csv
tests/test_review_pack.py17 автономный пакет разметки: отпечаток набора, порядок «расхождения первыми», встроенные данные разбираются как JSON, в странице нет ссылок на сеть, слияние вердиктов с построенной разметкой и сообщение о путях из чужого пакета

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

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

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 / head load_model, src/dxa/inference.py
Чекпоинт — «сырой» state_dict без метаданных ValueError: файл не содержит model_state_dict DXAQualityModel.load
Сбой в одиночном запросе /analyze, /analyze/detailed, /analyze/sr HTTP 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. Разметка выведена из оценки исследования, а не снимка. Экспертная таблица описывает исследование; перенос вердикта на снимок однозначен (область встречается один раз), но поштучной экспертной оценки снимков в наборе нет. Оценка качества модели упирается в качество этой разметки, а не только в объём данных. Для поштучной разметки в сервисе есть интерфейс /label (раздел 11.1), им ещё не пользовались.
  2. Мало данных. 251 уникальный снимок, 76 нарушений. Доверительные интервалы широкие (ROC-AUC 0.6726 [0.6367, 0.7086] по пяти seed'ам), поэтому оценка на закрытом наборе может отличаться.
  3. Эталон — та же таблица. Независимой истины нет, поэтому сравнение правил разметки частично благоприятствует варианту «только таблица» (раздел 12.3).
  4. В DICOM нет разметки областей интереса. Ни overlay, ни graphic annotation в файлах нет, поэтому корректность нанесённых областей измерения нельзя проверить прямым сравнением с эталоном: оценивается геометрия видимой зоны.
  5. Тип нарушения определяется признаками снимка, а не обученной моделью. Модель решает только бинарную задачу, поэтому в выгрузке тип содержателен только для размеченных снимков, а на инференсе эвристика на текущих данных всегда даёт «не уточнён» (раздел 8.2). Для честного мультикласса нужна разметка типов на уровне снимка; пять снимков имеют только код «не уточнён», потому что источник не указывает критерий.
  6. Три снимка размечены по пометке в имени файла — экспертная таблица их область не оценивала. Такие строки помечены признаком filename_fallback.
  7. Сторона бедра в семи исследованиях с единственным снимком не проверяема: теги Laterality пусты, оценка взята из единственного заполненного столбца таблицы.
  8. Порог определения области привязан к текущему оборудованию. Признак «ширина кадра» безошибочно работает на этом наборе, но при смене аппарата потребует калибровки.
  9. Числовые эвристики панели деталей не калиброваны. Их пороги рассчитаны на другой масштаб интенсивностей, поэтому в интерфейсе показываются измерения, а не вердикты.
  10. Рабочая точка порога даёт высокий recall при умеренной точности. На обучающем наборе при пороге, подобранном по F1, модель относит к нарушениям 204 строки из 482 (≈ 42 %) при фактической доле нарушений 30.3 %, что согласуется с precision 0.472. Порог выбран в пользу полноты: пропустить непригодное исследование дороже, чем показать лишнее.

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         разметка снимков по экспертной таблице
│   │   ├── manual_labels.py        ручная разметка: хранение вердиктов специалиста (/label)
│   │   ├── review_pack.py          автономный HTML-пакет разметки для специалиста (без сервера)
│   │   ├── 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, docker-compose.yml, 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; /label — ручная разметка
python -m src.dxa.discriminator         проверка вклада содержимого снимка

# проверки
./run.sh test                           pytest (272 теста)
node tests/browser/ui_check.js          проверки интерфейса (нужен сервер на 127.0.0.1:8123)
node tests/browser/ui_labeling.js       проверка интерфейса ручной разметки
PACK=review/doctor_review.html node tests/browser/review_pack.js   автономный пакет (без сервера)

# разметка специалистом для набора целиком
./run.sh review --out review/doctor_review.html                    файл-пакет для специалиста
./run.sh review --merge review/doctor.csv \
    --out labels/labels_images_reviewed.csv                        наложить вердикты на разметку

# контейнер (чекпоинт лежит внутри образа, монтировать пути не нужно)
docker compose up -d                            CPU-сервис в фоне, http://localhost:8000
docker compose --profile cuda up -d dxa-cuda    GPU-сервис (нужен nvidia-container-toolkit)
docker build -t dxa-quality .                   сборка CPU-образа напрямую
docker run -p 8000:8000 dxa-quality             запуск без compose

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