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-исследований: бинарная оценка «качественное / есть нарушение», определение анатомической области, тип нарушения, структурированный отчёт |
|---|---|
| Дата документа | 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.
+ +Сервис принимает денситометрическое исследование в формате DICOM и выполняет автоматизированный +контроль его качества: определяет анатомическую область, решает бинарную задачу «изображение пригодно +для клинической интерпретации / содержит нарушение качества», относит нарушение к категории и +формирует структурированный отчёт (XLSX/CSV) с одной строкой на изображение.
+ +Задача решается как цифровой помощник, а не как замена врача: результат предназначен для отбора +исследований, требующих внимания специалиста, и для единообразной регистрации нарушений. Области +анализа — поясничный отдел позвоночника и проксимальный отдел бедренной кости (левая и правая +стороны рассматриваются как отдельные снимки, поскольку сторона влияет на трактовку критериев).
+ +Решение работает полностью офлайн: изображения не передаются во внешние сервисы, все зависимости и +веса фиксированы, доступ в сеть при инференсе не требуется. Это прямое требование методики +(п. 3.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.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 |
| Обязательная контейнеризация, скрипт сборки и запуска в Linux | +Dockerfile (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 (данный документ) |
+ — |
| Слой | Файлы | Ответственность |
|---|---|---|
| Прикладной ядерный слой | src/dxa/ |
+ Чтение DICOM, предобработка, модель, метрики, определение области, разметка данных, обучение, пакетный инференс |
| Сервис | src/main.py |
+ FastAPI: загрузка чекпоинта при старте, маршруты анализа, пакетной обработки, экспорта, здоровья и карточки решения |
| Веб-интерфейс | src/api/static/ |
+ Загрузка файлов, таблица результатов, панель деталей, панель «О модели»; все подписи и метрики получает с сервера |
| Эвристики качества | src/quality/ |
+ Числовые измерения снимка (резкость, плотные включения, полнота и поворот области) для панели деталей |
| Инфраструктура | run.sh, Dockerfile, Dockerfile_cuda, Jenkinsfile |
+ Единая точка входа для сборки, обучения, инференса и тестов; сборка образа и выкладка |
Ниже — фактический путь данных от файла до строки отчёта. Ключевое свойство: обучение и сервис +используют один и тот же код предобработки и предсказания, поэтому расхождение между +офлайн-оценкой и работой API невозможно по построению, а не по договорённости.
+ +| DICOM | → | +Чтение пикселей и метаданных | → | +Предобработка 224×224 | → | +ResNet18 + линейная голова | +
|---|---|---|---|---|---|---|
| + | ||||||
| Логит → порог | → | +Область по кадру | → | +Числовые измерения | → | +Строка отчёта / ответ API | +
Порядок операций и точка принятия решения:
+StudyInstanceUID, SOPInstanceUID). Персональные данные не используются:
+ решение не зависит от их наличия.quality_class = 1, если логит выше порога. Порог подобран по F1 на валидации и
+ сохранён в чекпоинте (текущее значение −0.4930, что соответствует вероятности 0.379).README.md приведён полный набор проектных диаграмм (компоненты, потоки данных,
+последовательность, развёртывание). Они описывают целевую архитектуру, включая блоки, которые в текущей
+реализации не используются, поэтому в настоящем документе поток описан текстом по коду.| Показатель | Значение | Пояснение |
|---|---|---|
| Файлов на диске | 544 | в обучающем наборе (dataset_hack/НД_для_обучения) |
| Уникальных снимков | 252 | по пиксельному содержимому; 292 файла — побайтные дубли |
| Исследований | 100 | разбиение выполняется по исследованиям, а не по снимкам |
| Позвоночник / бедро правое / бедро левое / не определено | 99 / 79 / 73 / 1 | голосование по именам файлов после склейки дублей |
| Нарушений по экспертной таблице | 74 (29.4 %) | три снимка таблица не оценивала |
| Нарушений в рабочей разметке | 77 (30.6 %) | 74 по таблице плюс 3 снимка с пометкой в имени файла |
Дубли не пересекают границы исследований, конфликтов меток при склейке не возникает. Два побайтных +дубля названы по-разному, поэтому область определяется голосованием по именам файлов. Изучение +набора выявило две особенности, которые пришлось учесть в разметке: один и тот же снимок встречается +под несколькими именами (без склейки он попадал бы одновременно в оба класса), и имена файлов местами +расходятся с оценкой эксперта.
+ +Экспертная таблица описывает исследование, а не отдельный снимок. Однако каждая
+анатомическая область встречается в исследовании ровно один раз (после склейки дублей), поэтому
+вердикт исследования по области переносится на снимок однозначно — не требуется решать, какой из
+нескольких снимков «плохой». Так получены метки и типы нарушений (labels/labels_images.csv);
+каждый источник свидетельства сохранён в отдельном столбце, поэтому правило можно переиграть без
+повторного разбора данных.
Возможны были два правила: учитывать только экспертную таблицу либо дополнительно учитывать
+пометки _good/_bad, проставленные вручную в именах файлов. Пометки
+расходились с экспертом в 15 случаях из 252, поэтому правило выбиралось измерением: одно
+зафиксированное разбиение, пять seed'ов обучения, один эталон (табл. в разд. 12.3). Выбрано правило
+«только экспертная таблица».
| Часть | Снимков | Исследований | Нарушений | Файл |
|---|---|---|---|---|
| Обучение | 199 | 81 | 61 | labels/split_expert_seed42.json |
| Валидация | 53 | 19 | 16 |
Разбиение стратифицировано по эталону и фиксировано в файле: все сравниваемые варианты
+обучаются и оцениваются на одном и том же held-out наборе, что делает сравнения корректными.
+Снимки одного исследования не попадают одновременно в обе части — это исключает утечку, при которой
+метрики растут за счёт запоминания конкретных исследований. Наличие утечки и воспроизводимость
+разбиения проверяются тестами (tests/test_labels.py).
Предобработка описана декларативно в src/dxa/preprocess.py (неизменяемая конфигурация
+PreprocessConfig) и используется и при обучении, и при работе API. Параметры хранятся в
+чекпоинте и восстанавливаются из него, поэтому вход модели не может разойтись между двумя режимами.
| Параметр | Значение | Назначение |
|---|---|---|
norm | percentile | приведение интенсивностей к [0, 1]; допустимо также minmax |
p_low / p_high | 0.5 / 99.5 | отсекаемые перцентили — устойчивость к выбросам и шуму |
imagenet_norm | true | стандартизация mean (0.485, 0.456, 0.406), std (0.229, 0.224, 0.225), как ожидает предобученный backbone |
input_size | 224 | сторона квадратного входа сети |
Порядок операций над одним файлом:
+pydicom.dcmread и pixel_array в float32;
+ многокадровое изображение усредняется по кадрам, иная размерность — ошибка.RescaleSlope и
+ RescaleIntercept; для MONOCHROME1 яркость инвертируется.hi ≤ lo).uint8, дублирование одного канала
+ в три, resize 224×224 билинейно, перевод в CHW, деление на 255, затем нормировка ImageNet.Если в чекпоинте нет блока preprocess (старый файл), применяются значения по умолчанию —
+percentile с нормировкой ImageNet; устаревший профиль minmax без ImageNet
+доступен флагом --no-imagenet-norm.
uint8, затем нормировка» сохранён намеренно: он совпадает
+с историческим кодом проекта, поэтому смена реализации не сдвинула распределение входа и не обесценила
+ранее подобранный порог.| Элемент | Реализация |
|---|---|
| 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. Без неё логиты смещены, вероятности скучены у нуля и
+ подобранный порог теряет смысл |
| Параметр | Значение по умолчанию | Комментарий |
|---|---|---|
| Оптимизатор | 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 не ниже
+заданного.
Рабочий чекпоинт. Лучшая эпоха — 39 из прогона в 64 эпохи (обучение остановлено по терпению), +порог логита −0.4930 (вероятность 0.379). Метрики этой эпохи — в разделе 12, честная оценка варианта на +пяти seed'ах — ROC-AUC 0.6764.
+ +Чекпоинт самодостаточен: кроме весов в нём лежат архитектура, параметры предобработки, порог и +происхождение данных, поэтому инференс не может рассинхронизироваться с обучением.
+ +| Поле | Содержимое |
|---|---|
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).
Область определяется по содержимому снимка, а не по имени файла: на закрытом наборе соглашение об
+именах может отсутствовать. Решение принимается по геометрии кадра (resolve_region в
+src/dxa/inference.py) независимо от модели качества; обученная вспомогательная голова
+области привлекается только как уточнение.
Признаки считает функция heuristic_signals по маске яркой (костной) ткани с порогом по
+95-му перцентилю:
| Признак | Что измеряет |
|---|---|
width | ширина кадра в пикселях — основной признак области на этом оборудовании (у позвоночника 300 px, у бедра 280 px) |
bbox_aspect | отношение высоты яркой области к её ширине (вытянутость) |
left_right_ratio | перевес светимости левой половины над правой — наклон в сторону бедра |
symmetry | симметрия изображения относительно вертикальной оси |
Порог ширины задан константой SPINE_MIN_WIDTH = 295. Порядок решений:
spine, уверенность 0.8;left_right_ratio > 1.3 даёт
+ hip_right, < 0.7 — hip_left, уверенность 0.6;hip с уверенностью 0.4.Если ширина кадра неизвестна, используется предсказание головы, а затем форма яркой области
+(bbox_aspect и symmetry).
Точность. Сопоставление с областью из рабочей разметки (252 снимка, раздел 4):
+ +| Что проверялось | Результат |
|---|---|
| Позвоночник | 99 / 99 |
| Сторона бедра (правое / левое) | 135 / 152 (88.8 %) |
| Итого по всем областям | 234 / 251 (93.2 %) |
Различение позвоночника и бедра по ширине кадра работает безошибочно, а сторона бедра определяется
+менее надёжно: перевес светимости путает левое и правое в 17 случаях из 152. Сторона не проверялась по
+тегам DICOM — Laterality в наборе пуст, — поэтому это ограничение (раздел 17, п. 7), а не
+измеренная ошибка модели.
Коды типов нарушений собраны в одном модуле 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 | Ошибка разметки | spine | condition_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.
Экспертная таблица кодирует не все девять кодов, а пять: positioning,
+axis_deviation, artifact, rotation, roi_incorrect.
+Распределение в рабочей разметке (в четырёх из 77 нарушений указано по два критерия, поэтому сумма
+больше 77):
| Код | Снимков |
|---|---|
rotation | 36 |
artifact | 17 |
axis_deviation | 10 |
roi_incorrect | 7 |
positioning | 6 |
unspecified | 5 |
Коды motion, incomplete_anatomy и labeling_error в разметке не
+встречаются: таблица их не кодирует, хотя словарь их знает ради критериев
+condition_doctor.txt. Пять снимков имеют только unspecified, потому что
+источник не указывает критерий.
Модель решает только бинарную задачу, поэтому тип для снимка с нарушением выбирает функция
+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.Один файл DICOM (для API — один HTTP-загрузкой, для CLI — файл или каталог с рекурсивным обходом). +Исследование может содержать несколько снимков — каждый обрабатывается отдельно и даёт отдельную +строку отчёта. Перед обработкой файл читается целиком; при ошибке чтения строка получает статус +сбоя, а пакет продолжается.
+ +Состав столбцов проверен на реальных прогонах — и через CLI, и через /api/v1/export.
+Обязательные восемь столбцов задания совпадают в обоих путях по составу и порядку; дополнительно
+пишутся confidence и violation_reason, а CLI-инференс добавляет одиннадцатый
+столбец region_confidence. Таблица ниже отражает CLI-вывод; в ответе
+/api/v1/export последнего столбца нет.
| № | Столбец | Тип | Содержимое |
|---|---|---|---|
| 1 | path_to_study | строка | путь к исследованию (для загрузки через API — upload://имя) |
| 2 | study_uid | строка | StudyInstanceUID из DICOM |
| 3 | image_uid | строка | SOPInstanceUID из DICOM |
| 4 | anatomical_region | строка | spine / hip_left / hip_right / hip |
| 5 | quality_class | целое | 0 — качественное, 1 — есть нарушение |
| 6 | violation_type | строка | код нарушения или пусто |
| 7 | processing_status | строка | Success либо Failure: <причина> |
| 8 | time_of_processing | вещественное | время обработки снимка, секунды |
| 9 | confidence | вещественное | уверенность модели в решении |
| 10 | violation_reason | строка | текстовое пояснение к отнесённой категории |
| 11 | region_confidence | вещественное | уверенность определения области |
POST /api/v1/analyze этого столбца не содержит — при интеграции время измеряется на
+стороне вызывающей системы (см. разд. 10 и 13).Сервис на 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 |
/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.
Два поля заслуживают пояснения при интеграции:
+violation_type_is_heuristic = true — тип нарушения определён по измеряемым признакам
+ снимка, а не предсказан моделью; модель решает только бинарную задачу. Флаг отдаётся клиенту
+ явно, чтобы это различие не терялось в интеграции.quality_label, violation_type_label, violation_type_note —
+ готовые подписи для интерфейса. Они приходят с сервера: клиент не держит собственных копий
+ словаря, поэтому подписи не могут разойтись с кодами./api/v1/analyze/detailedДополнительно к предыдущему набору: image (визуализация снимка), mask
+(маска зоны измерения, при запросе), reasons, metrics_note,
+view_quality, а также измерения по областям: spine_completeness,
+hip_completeness, hip_rotation.
Интерфейс предназначен для ручной проверки работы сервиса и для демонстрации: загрузка файлов, +таблица результатов со статистикой, панель деталей по выбранной строке и панель «О модели» с +источником загруженного чекпоинта.
+ +
+
+
+ Отдельный принцип — интерфейс не утверждает больше, чем известно решению:
+
+ /api/v1/model.Оценка получена на фиксированном разбиении по исследованиям (обучение 199 снимков / 81 исследование, +валидация 53 снимка / 19 исследований, 16 нарушений), пять seed'ов обучения, эталон — вердикт +эксперта из таблицы. Приоритетные по заданию метрики приведены с 95 % доверительными интервалами.
+ +| Метрика | Значение | 95 % ДИ | Комментарий |
|---|---|---|---|
| ROC-AUC | 0.6764 | [0.6309, 0.7218] | приоритетная метрика задания |
| PR-AUC | 0.4759 | [0.4141, 0.5377] | базовый уровень при доле нарушений 30 % — около 0.30 |
| F1 | 0.5676 | [0.5270, 0.6082] | порог подбирался по F1 на той же валидации, поэтому значение смещено вверх |
| Recall / Precision | 0.700 / 0.486 | — | рабочая точка выбранного порога |
| Что измерено | Значение | Как получено |
|---|---|---|
| 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. Это означает, что модель использует содержимое +снимка, а не только область.
+ +| Метрика (эталон) | только таблица | с суффиксами имён |
|---|---|---|
| ROC-AUC | 0.6764 [0.6309, 0.7218] | 0.6199 [0.5840, 0.6559] |
| PR-AUC | 0.4759 [0.4141, 0.5377] | 0.4046 [0.3702, 0.4391] |
| F1 | 0.5676 [0.5270, 0.6082] | 0.5426 [0.5073, 0.5778] |
| Recall / Precision | 0.700 / 0.486 | 0.863 / 0.404 |
Парная разница (только таблица минус с суффиксами): ROC-AUC +0.0564 [+0.0403, +0.0725],
+PR-AUC +0.0713 [+0.0398, +0.1028] — знаки +++++, то есть преимущество на всех пяти
+seed'ах. Принято правило «только экспертная таблица». Оговорка, важная для интерпретации: эталон — та
+же таблица, поэтому вариант, обучавшийся на ней, находится в выигрышном положении; значимо здесь то,
+что добавление ненадёжных пометок согласие с экспертом снижает, а не повышает.
Измерения выполнены на рабочей машине разработчика (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 минут на исследование»: даже если +исследование содержит максимальные по заданию три снимка, обработка занимает доли секунды, а +трёхминутный бюджет расходуется практически только на передачу файлов и накладные расходы +интеграции. Запас более чем трёхкратный, поэтому узким местом решение не ограничивает.
+ +| Ресурс | Минимальная конфигурация | Рекомендуемая |
|---|---|---|
| Процессор | 2 ядра x86-64 или ARM64 | 4 ядра и более; инференс векторизован и масштабируется по кадрам |
| Оперативная память | 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 МБ) монтируется отдельно.
| Элемент | Значение |
|---|---|
| Базовый образ | 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 с ожидаемым
+набором столбцов. Сквозная проверка выполнялась в контейнере на той же машине, где снимались
+измерения производительности.
backbone, head, параметры
+ предобработки, threshold, а также labels_csv и split_file.
+ Поэтому по файлу видно, на какой разметке и на каком разбиении получена модель, а инференс не
+ может рассинхронизироваться с обучением.Jenkinsfile выполняет три шага: сборка образа, публикация в реестр и выкладка в
+кластер. Скрипт run.sh — единая точка входа для разработки и эксплуатации: обучение,
+разметка, приведение имён файлов, инференс, сервис, сравнение вариантов разметки и тесты
+(перечень команд — приложение Б).
Тесты — pytest, 209 проверок в шести файлах; запуск — ./run.sh test или
+python -m pytest tests/ -q. Сверка выполнена на дату документа: 209 passed.
| Файл | Тестов | Что проверяет |
|---|---|---|
tests/test_labels.py | 38 | +разбор имён, определение области и метки по имени, стратифицированное разбиение по + исследованиям, отсутствие утечки, фиксация и переиспользование разбиения в файле, сверка с + реальным датасетом |
tests/test_excel_labels.py | 50 | +разметка по экспертной таблице: чтение критериев, правило «1 = нарушение», голосование по
+ области, перенос оценки на единственное бедро, три правила метки
+ (table / union / expert), их согласованность и подключение
+ к обучению |
tests/test_rename_files.py | 36 | +приведение имён DICOM: разбор и канонизация, поиск свободного номера при конфликте, отказ + угадывать область, цикл «применить → откатить» |
tests/test_preprocess_and_model.py | 32 | +конфигурация и нормировка, сборка тензора и нормировка ImageNet, метрики, подбор порога, + контракт модели, BatchNorm при заморозке, roundtrip чекпоинта, проверка на реальных снимках |
tests/test_violations.py | 28 | +единый словарь типов: приведение устаревших значений, подписи, коды SR и каталог для API, + согласованность с критериями экспертной таблицы |
tests/test_api_contract.py | 25 | +поля ответов, которые читает веб-интерфейс: базовый и детальный анализ, здоровье и карточка + модели, выгрузка 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 |
+ отсутствие обращений страницы к внешним хостам |
Требование задания — отсутствие необработанных исключений: сбой на одном файле фиксируется в отчёте и +не прерывает пакет. Ниже — фактические режимы отказа и способ обработки.
+ +| Ситуация | Поведение | Где обрабатывается |
|---|---|---|
| Ошибка чтения или обработки файла (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 и отклоняет запросы анализа. Молчаливая неверная оценка вместо
+явной ошибки была бы опаснее в медицинском контуре.Ограничения сформулированы так, чтобы читатель мог оценить границы применимости решения, а не +только увидеть итоговые метрики.
+ +filename_fallback.Laterality пусты, оценка взята из единственного заполненного столбца таблицы.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.