Initial commit: project structure, scripts and documentation

This commit is contained in:
2026-08-22 13:17:29 +03:00
commit 5042305df0
54 changed files with 14166 additions and 0 deletions
+975
View File
@@ -0,0 +1,975 @@
% ============================================================================
% ЛАБОРАТОРНАЯ РАБОТА №1
% АУДИТ И ПОДГОТОВКА МЕДИЦИНСКИХ ДАННЫХ
% ============================================================================
% КОМПИЛЯЦИЯ: pdflatex (ДВА ПРОХОДА)
% ============================================================================
\documentclass[12pt,a4paper]{article}
% ============================================================================
% ПРЕАМБУЛА
% ============================================================================
\usepackage[utf8]{inputenc}
\usepackage[T2A]{fontenc}
\usepackage[russian]{babel}
\usepackage{amsmath,amssymb,amsfonts}
\usepackage{graphicx}
\usepackage{hyperref}
\usepackage{listings}
\usepackage{xcolor}
\usepackage{geometry}
\usepackage{booktabs}
\usepackage{longtable}
\usepackage{caption}
\usepackage{subcaption}
\usepackage{titlesec}
\usepackage{float}
\usepackage{array}
\usepackage{multirow}
\usepackage{enumitem}
\geometry{left=2.5cm,right=2cm,top=2cm,bottom=2cm}
% --- Гиперссылки ---
\hypersetup{
colorlinks=true,
linkcolor=blue,
urlcolor=blue,
pdfauthor={Студент},
pdftitle={Лабораторная работа N1}
}
% --- Листинги (максимально упрощённые) ---
\lstset{
basicstyle=\ttfamily\footnotesize,
keywordstyle=\color{blue},
commentstyle=\color{green!60!black},
stringstyle=\color{red},
breaklines=true,
frame=single,
numbers=left,
numberstyle=\tiny,
tabsize=4,
language=Python,
extendedchars=true,
inputencoding=utf8,
}
% --- Заголовки ---
\titleformat{\section}{\Large\bfseries\centering}{}{0pt}{}
\titleformat{\subsection}{\large\bfseries}{}{0pt}{}
\titleformat{\subsubsection}{\normalsize\bfseries}{}{0pt}{}
% --- Список литературы ---
\renewcommand{\refname}{Список использованной литературы}
\begin{document}
% ============================================================================
% ТИТУЛЬНЫЙ ЛИСТ
% ============================================================================
\begin{titlepage}
\begin{center}
\vspace*{0.5cm}
{\Large \textbf{ФГБУ <<НМИЦ онкологии им. Н.Н. Блохина>> Минздрава России}}
\vspace{0.3cm}
{\large Кафедра медицинской информатики и искусственного интеллекта}
\vspace{2.5cm}
{\Huge \textbf{ЛАБОРАТОРНАЯ РАБОТА №1}}
\vspace{0.5cm}
{\huge \textbf{Аудит и подготовка медицинских данных}}
\vspace{0.5cm}
{\Large для задач искусственного интеллекта в онкологии}
\vspace{2.5cm}
\begin{tabular}{p{0.4\textwidth}p{0.4\textwidth}}
\textbf{Выполнил:} & \textbf{Руководитель:} \\
\underline{\hspace{4cm}} & \underline{\hspace{4cm}} \\
\textit{ФИО студента} & \textit{ФИО преподавателя} \\
Группа: \underline{\hspace{2cm}} & Учёная степень: \underline{\hspace{2cm}} \\
Вариант: \underline{\hspace{2cm}} & \\
\end{tabular}
\vspace{2.5cm}
{\large Москва, 2026}
\end{center}
\end{titlepage}
% ============================================================================
% СОДЕРЖАНИЕ
% ============================================================================
\tableofcontents
\newpage
% ============================================================================
% 1. ВВЕДЕНИЕ
% ============================================================================
\section{Введение}
\subsection{Цель работы}
Сформировать у слушателей профессиональные компетенции в области аудита, предобработки и структурирования медицинских изображений для последующего обучения моделей искусственного интеллекта в задачах диагностики и лечения онкологических заболеваний.
\textbf{Задачи работы:}
\begin{enumerate}
\item Провести аудит структуры медицинского датасета в формате NRRD.
\item Реализовать конвертацию полигональной разметки в бинарные маски.
\item Выполнить распаковку 3D-данных в 2D-срезы формата YOLO.
\item Создать конфигурационные файлы \texttt{predefined\_classes.txt} и \texttt{data.yaml}.
\item Проанализировать роль клинического эксперта в подготовке данных.
\end{enumerate}
\subsection{Клинический контекст}
В рамках лабораторной работы используется датасет \texttt{1\_2236040\_zno\_cyst\_AF}, представляющий собой мультимодальные медицинские данные пациентов с новообразованиями почек. Исследование направлено на автоматическую детекцию и сегментацию анатомических структур почек и патологических образований.
\textbf{Объекты исследования:}
\begin{itemize}
\item Почки (правая и левая)
\item Сегменты почек (верхний, средний, нижний)
\item Патологические образования: злокачественные опухоли, доброкачественные опухоли, кисты, абсцессы
\end{itemize}
\textbf{Клиническая значимость:} Ранняя диагностика и точная дифференцировка патологических образований почек критически важны для выбора оптимальной тактики лечения и прогнозирования исходов. Внедрение методов искусственного интеллекта позволяет автоматизировать анализ медицинских изображений, снизить нагрузку на врачей и повысить точность диагностики.
\subsection{Обзор форматов данных}
\subsubsection{DICOM (Digital Imaging and Communications in Medicine)}
DICOM -- международный стандарт создания, хранения и передачи медицинской визуальной информации.
\textbf{Ключевые особенности:}
\begin{itemize}
\item Каждый файл содержит не только матрицу пикселей, но и богатый набор метаданных.
\item Метаданные включают информацию о пациенте, модальности, параметрах сканирования, оборудовании.
\item Аннотации областей интереса (ROI) хранятся в отдельных файлах DICOM-RT Structure Set.
\item Формат поддерживает различные модальности: КТ, МРТ, ПЭТ, маммография и др.
\end{itemize}
\textbf{Структура DICOM-файла:}
\begin{itemize}
\item \textbf{Заголовок (метаданные)}: информация о пациенте, исследовании, серии, параметрах сканирования.
\item \textbf{Тело (пиксельные данные)}: матрица интенсивностей.
\end{itemize}
\subsubsection{NRRD (Nearly Raw Raster Data)}
NRRD -- формат, широко используемый в научных исследованиях для хранения многомерных растровых данных.
\textbf{Ключевые особенности:}
\begin{itemize}
\item Простая структура: текстовый заголовок + бинарные данные.
\item Поддержка 3D и 4D массивов.
\item Минимальные метаданные, но критически важные.
\item Параметры \texttt{space directions} определяют направление осей.
\item Параметр \texttt{space origin} задаёт начало координат.
\end{itemize}
\textbf{Пример структуры NRRD-файла:}
\begin{verbatim}
NRRD0004
type: short
dimension: 3
sizes: 512 512 100
spacings: 0.5 0.5 1.0
space: left-posterior-superior
space directions: (1,0,0) (0,1,0) (0,0,1)
space origin: (0,0,0)
... binary data ...
\end{verbatim}
\subsubsection{Сравнение форматов}
\begin{table}[H]
\centering
\caption{Сравнение форматов DICOM и NRRD}
\begin{tabular}{p{3.5cm}|p{4.5cm}|p{4.5cm}}
\toprule
\textbf{Характеристика} & \textbf{DICOM} & \textbf{NRRD} \\
\midrule
Структура & Файл + метаданные & Заголовок + бинарные данные \\
Размер метаданных & Большой (медицинский контекст) & Минимальный (геометрия) \\
Поддержка 3D & Да (через серии) & Да (родная) \\
Читаемость & Требуется специализированное ПО & Поддерживается многими библиотеками \\
Разметка & Отдельные файлы (RT Structure) & В том же файле или отдельном .seg \\
Применение & Клиническая практика & Научные исследования \\
\bottomrule
\end{tabular}
\end{table}
% ============================================================================
% 2. ТЕОРЕТИЧЕСКАЯ ЧАСТЬ
% ============================================================================
\section{Теоретическая часть}
\subsection{Формат DICOM и DICOM-RT Structure Set}
DICOM-RT Structure Set (DICOM-RTSS) -- это специальный объект DICOM, предназначенный для хранения структурных наборов, включая контуры областей интереса (ROI).
\textbf{Структура DICOM-RTSS:}
\begin{itemize}
\item \textbf{ROI Contour Sequence}: последовательность контуров для каждой ROI.
\item \textbf{Contour Sequence}: точки контура в координатах DICOM.
\item \textbf{ROI Observations}: информация о каждой ROI (тип, название).
\end{itemize}
\textbf{Формат хранения контуров:}
\begin{verbatim}
(3006, 0039) Contour Image Sequence
(3006, 0040) Contour Geometric Type: CLOSED_PLANAR
(3006, 0042) Contour Number: 1
(3006, 0050) Contour Data: [x1, y1, z1, x2, y2, z2, ...]
\end{verbatim}
Координаты контуров хранятся в физических координатах DICOM (мм) и могут быть преобразованы в пиксельные координаты с использованием аффинных преобразований.
\subsection{Формат NRRD: пространственная ориентация}
Для работы с 3D-данными критически важно сохранение пространственной ориентации. Параметры \texttt{space directions} и \texttt{space origin} определяют геометрию объёма.
\textbf{Параметры ориентации:}
\begin{itemize}
\item \texttt{space directions}: матрица направления осей.
\item \texttt{space origin}: координаты начала координат.
\item \texttt{spacings}: расстояния между пикселями по каждой оси.
\end{itemize}
\textbf{Математическое представление:}
\begin{equation}
\mathbf{p}_{world} = \mathbf{D} \cdot \mathbf{p}_{voxel} + \mathbf{o}
\label{eq:orientation}
\end{equation}
где:
\begin{itemize}
\item $\mathbf{p}_{world}$ -- координаты в физическом пространстве.
\item $\mathbf{D}$ -- матрица направления ($3 \times 3$).
\item $\mathbf{p}_{voxel}$ -- координаты вокселя.
\item $\mathbf{o}$ -- смещение (space origin).
\end{itemize}
\textbf{Пример матрицы направления:}
\begin{equation}
\mathbf{D} = \begin{pmatrix}
-1 & 0 & 0 \\
0 & 1 & 0 \\
0 & 0 & 1
\end{pmatrix}
\end{equation}
Потеря этих параметров при конвертации делает невозможным использование данных для обучения 3D-нейронных сетей.
\subsection{Пайплайн подготовки данных для YOLO}
Стандартный пайплайн подготовки данных включает следующие этапы:
\begin{enumerate}
\item \textbf{Аудит данных}: проверка структуры, целостности, соответствия форматам.
\item \textbf{Извлечение срезов}: конвертация 3D-данных в 2D-срезы.
\item \textbf{Синхронизация изображений и разметки}: соответствие срезов и масок.
\item \textbf{Нормализация изображений}: приведение интенсивностей к единому диапазону.
\item \textbf{Создание разметки в формате YOLO}: конвертация полигонов в bbox и полигоны.
\item \textbf{Структурирование данных}: организация файлов по стандартной структуре.
\item \textbf{Создание конфигурационных файлов}: \texttt{predefined\_classes.txt} и \texttt{data.yaml}.
\end{enumerate}
\textbf{Стандартная структура директорий:}
\begin{verbatim}
experiment/
- images/ # 2D-срезы для обучения
- images_labels/ # Bbox-разметка (YOLO формат)
- polygons/ # Полигоны для сегментации
- images_result/ # Визуализация результатов
- masks_sam/ # Бинарные маски
- predefined_classes.txt # Схема классов
- data.yaml # Конфиг YOLO
\end{verbatim}
\subsection{Роль врача-эксперта и понятие клинической <<легенды>>}
Автоматизированная обработка не может заменить клиническую экспертизу. Под клинической <<легендой>> датасета понимается полное и точное описание данных.
\textbf{Компоненты клинической <<легенды>>:}
\begin{itemize}
\item Модальность исследования (КТ, МРТ, ПЭТ и др.)
\item Протокол сканирования
\item Подтверждённый диагноз
\item Критерии включения и исключения
\item Правила разметки областей интереса
\item Источник данных
\item Версия программного обеспечения сканера
\end{itemize}
\textbf{Риски отсутствия экспертизы:}
\begin{enumerate}
\item \textbf{Формирование ложных паттернов}: нейросеть может научиться распознавать не патологию, а артефакты сканирования или особенности оборудования (<<shortcut learning>>).
\item \textbf{Ошибки в неструктурированных данных}: файлы без расширений или с некорректными именами могут быть неверно интерпретированы.
\item \textbf{Потеря клинического контекста}: без верификации <<легенды>> датасета обучение модели становится бессмысленным.
\end{enumerate}
\textbf{Обязательные меры:}
\begin{itemize}
\item Верификация выборки квалифицированным врачом-радиологом или онкологом.
\item Документирование всех этапов подготовки данных.
\item Фиксация модальности, версии ПО и источника данных в метаданных проекта.
\end{itemize}
% ============================================================================
% 3. ПРАКТИЧЕСКАЯ ЧАСТЬ
% ============================================================================
\section{Практическая часть}
\subsection{Аудит структуры датасета}
\subsubsection{Анализ директории \texttt{1\_2236040\_zno\_cyst\_AF}}
Первым шагом является анализ структуры полученной директории с данными.
\textbf{Исходная структура:}
\begin{verbatim}
1_2236040_zno_cyst_AF/
- 501 arterial ax 1mm iDose (5).nrrd # Исходные 3D данные
- Segmentation.seg.nrrd # Сегментационная маска
- dcm/ # DICOM-срезы (1042 файла)
- images/ # PNG-срезы (300 файлов)
- images_labels/ # Bbox-разметка
- polygons/ # Полигоны для сегментации
- images_result/ # Визуализация результатов
- masks_sam/ # Маски SAM
\end{verbatim}
\textbf{Статистика данных:}
\begin{table}[H]
\centering
\caption{Статистика датасета}
\begin{tabular}{l|r}
\toprule
\textbf{Тип данных} & \textbf{Количество} \\
\midrule
DICOM-файлы & 1042 \\
PNG-изображения & 300 \\
Файлы разметки (bbox) & 300 \\
Файлы разметки (polygons) & 300 \\
Классы & 12 \\
\bottomrule
\end{tabular}
\end{table}
\textbf{Выявленные особенности:}
\begin{enumerate}
\item Исходные данные представлены в формате NRRD (изображение и сегментационная маска).
\item DICOM-файлы сгенерированы из NRRD.
\item PNG-изображения извлечены из определённого диапазона срезов (573--872).
\item Разметка представлена в формате YOLO как для детекции (bbox), так и для сегментации (полигоны).
\item Присутствуют \texttt{.gitkeep} файлы для сохранения пустых директорий.
\end{enumerate}
\subsubsection{Проверка целостности данных}
Для проверки целостности необходимо:
\begin{enumerate}
\item Сопоставить количество изображений и файлов разметки.
\item Проверить соответствие идентификаторов срезов.
\item Валидировать форматы координат в файлах разметки.
\item Проверить отсутствие повреждённых файлов.
\end{enumerate}
\textbf{Пример проверки соответствия:}
\begin{lstlisting}
from pathlib import Path
images_dir = Path("data/1_2236040_zno_cyst_AF/images")
labels_dir = Path("data/1_2236040_zno_cyst_AF/images_labels")
images = set(f.stem for f in images_dir.glob("*.png"))
labels = set(f.stem for f in labels_dir.glob("*.txt"))
missing_labels = images - labels
missing_images = labels - images
print(len(images), len(labels))
print(len(missing_labels), len(missing_images))
\end{lstlisting}
\subsection{Конвертация полигонов в бинарные маски}
\subsubsection{Извлечение координат из DICOM-RT}
Для конвертации полигонов из DICOM-RT Structure Set в бинарные маски используется библиотека \texttt{pydicom}.
\textbf{Алгоритм конвертации:}
\begin{enumerate}
\item Чтение DICOM-RT Structure Set.
\item Извлечение контуров областей интереса (ROI).
\item Преобразование физических координат в пиксельные.
\item Растеризация полигонов в бинарные маски.
\item Визуальный контроль качества.
\end{enumerate}
\textbf{Реализация:}
\begin{lstlisting}
import numpy as np
from skimage.draw import polygon
def rasterize_contour(coords, shape):
mask = np.zeros(shape, dtype=np.uint8)
rr, cc = polygon(coords[:, 1], coords[:, 0], shape)
mask[rr, cc] = 1
return mask
\end{lstlisting}
\subsubsection{Визуальный контроль качества}
После конвертации необходимо визуально проверить качество полученных масок.
\textbf{Скрипт для визуализации:}
\begin{lstlisting}
import matplotlib.pyplot as plt
def show_mask(image, mask):
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
axes[0].imshow(image, cmap='gray')
axes[0].set_title('Original')
axes[0].axis('off')
axes[1].imshow(image, cmap='gray')
axes[1].imshow(mask, cmap='jet', alpha=0.5)
axes[1].set_title('Mask')
axes[1].axis('off')
plt.show()
\end{lstlisting}
\textbf{Типичные проблемы:}
\begin{itemize}
\item <<Разрывы>> контуров из-за повреждённых тегов.
\item Смещение контуров из-за ошибок в аффинных преобразованиях.
\item Пропущенные срезы в многослойных структурах.
\end{itemize}
\subsection{Распаковка NRRD в 2D-срезы}
\subsubsection{Загрузка NRRD-файлов}
Для работы с NRRD-файлами используется библиотека \texttt{nrrd}.
\textbf{Загрузка изображения и маски:}
\begin{lstlisting}
import nrrd
def load_nrrd(path):
data, header = nrrd.read(path)
return data, header
img_data, img_header = load_nrrd("image.nrrd")
seg_data, seg_header = load_nrrd("seg.seg.nrrd")
\end{lstlisting}
\subsubsection{Синхронизация срезов}
Критически важно сохранить соответствие между срезами изображения и маски.
\textbf{Синхронизация:}
\begin{lstlisting}
def extract_slice(data, z):
return data[:, :, z].T
img_slice = extract_slice(img_data, 600)
seg_slice = extract_slice(seg_data, 600)
\end{lstlisting}
\subsubsection{Сохранение пространственной ориентации}
При конвертации необходимо сохранить параметры ориентации.
\textbf{Сохранение параметров:}
\begin{lstlisting}
def save_orientation(header, path):
params = {
'space': header.get('space', ''),
'space_directions': header.get('space directions', ''),
'space_origin': header.get('space origin', ''),
'spacings': header.get('spacings', ''),
}
with open(path, 'w') as f:
for k, v in params.items():
f.write(k + ': ' + str(v) + '\n')
\end{lstlisting}
\subsection{Создание конфигурационных файлов}
\subsubsection{\texttt{predefined\_classes.txt}}
Файл \texttt{predefined\_classes.txt} содержит полную схему классов для эксперимента.
\textbf{Содержимое файла:}
\begin{lstlisting}
right_kidney_ID1
right_kidney_upper_segment_ID2
right_kidney_middle_segment_ID3
right_kidney_lower_segment_ID4
left_kidney_ID5
left_kidney_upper_segment_ID6
left_kidney_middle_segment_ID7
left_kidney_lower_segment_ID8
malignant_tumor_ID9
benign_tumor_ID10
cyst_ID11
abscess_ID12
\end{lstlisting}
\textbf{Формат:} один класс на строку, позиция в файле = YOLO index (0-based).
\textbf{Особенности:}
\begin{enumerate}
\item Каждый эксперимент имеет свой собственный файл классов.
\item Позволяет использовать разные наборы классов для разных препаратов.
\item Скрипт \texttt{ttv\_freq2.py} автоматически загружает этот файл.
\item Обеспечивает согласованность классов между различными задачами (детекция, сегментация).
\end{enumerate}
\subsubsection{\texttt{data.yaml}}
Файл \texttt{data.yaml} является основным конфигурационным файлом для YOLO.
\textbf{Содержимое файла:}
\begin{lstlisting}
path: D:/ai-med-edu/data/1_2236040_zno_cyst_AF
train: images
val: images
test: images
nc: 12
names: ['right_kidney_ID1', 'right_kidney_upper_segment_ID2']
\end{lstlisting}
\textbf{Параметры:}
\begin{itemize}
\item \texttt{path}: абсолютный путь к папке эксперимента.
\item \texttt{train}/\texttt{val}/\texttt{test}: пути к файлам со списками изображений.
\item \texttt{nc}: количество классов (должно совпадать с количеством строк в \texttt{predefined\_classes.txt}).
\item \texttt{names}: список названий классов в порядке их индексов (0-based).
\end{itemize}
% ============================================================================
% 4. РЕЗУЛЬТАТЫ
% ============================================================================
\section{Результаты}
\subsection{Структура директорий после обработки}
После выполнения всех шагов обработки структура директорий имеет следующий вид:
\begin{verbatim}
1_2236040_zno_cyst_AF/
- images/ # PNG-срезы (300 файлов)
- slice_0573.png
- slice_0574.png
- ...
- images_labels/ # Bbox-разметка
- slice_0573.txt
- slice_0574.txt
- ...
- polygons/ # Полигоны
- slice_0573.txt
- slice_0574.txt
- ...
- images_result/ # Визуализация
- slice_0573_preview.png
- slice_0574_preview.png
- ...
- masks_sam/ # Бинарные маски
- slice_0573_class_8_obj_0000.png
- ...
- dcm/ # DICOM-срезы (1042 файла)
- slice_0000.dcm
- slice_0001.dcm
- ...
- predefined_classes.txt # Схема классов (12 классов)
- data.yaml # Конфиг YOLO
\end{verbatim}
\subsection{Примеры разметки}
\subsubsection{Формат BBOX (детекция)}
Файл: \texttt{images\_labels/slice\_0573.txt}
\begin{verbatim}
8 0.354492 0.541992 0.017578 0.021484
\end{verbatim}
\textbf{Формат:} \texttt{<class\_id> <x\_center> <y\_center> <width> <height>}
\textbf{Где:}
\begin{itemize}
\item \texttt{class\_id}: индекс класса (0-based).
\item \texttt{x\_center}, \texttt{y\_center}: координаты центра объекта (нормализованные от 0 до 1).
\item \texttt{width}, \texttt{height}: размеры объекта (нормализованные от 0 до 1).
\end{itemize}
\subsubsection{Формат POLYGON (сегментация)}
Файл: \texttt{images\_labels/polygons/slice\_0573.txt}
\begin{verbatim}
8 0.349609 0.531250 0.345703 0.535156 0.345703 0.542969 ...
\end{verbatim}
\textbf{Формат:} \texttt{<class\_id> <x1> <y1> <x2> <y2> ... <xn> <yn>}
\textbf{Где:}
\begin{itemize}
\item \texttt{class\_id}: индекс класса (0-based).
\item \texttt{x1}, \texttt{y1}, \texttt{x2}, \texttt{y2}, ...: координаты точек полигона (нормализованные от 0 до 1).
\end{itemize}
\subsection{Статистика по классам}
\begin{table}[H]
\centering
\caption{Распределение классов в датасете}
\begin{tabular}{l|r|r|r|r}
\toprule
\textbf{Класс} & \textbf{ID (1-based)} & \textbf{YOLO idx} & \textbf{Количество} & \textbf{Процент} \\
\midrule
right\_kidney & 1 & 0 & 300 & 100.0\% \\
right\_kidney\_upper\_segment & 2 & 1 & 35 & 11.7\% \\
right\_kidney\_middle\_segment & 3 & 2 & 55 & 18.3\% \\
right\_kidney\_lower\_segment & 4 & 3 & 47 & 15.7\% \\
left\_kidney & 5 & 4 & 26 & 8.7\% \\
left\_kidney\_upper\_segment & 6 & 5 & 28 & 9.3\% \\
left\_kidney\_middle\_segment & 7 & 6 & 36 & 12.0\% \\
left\_kidney\_lower\_segment & 8 & 7 & 44 & 14.7\% \\
malignant\_tumor & 9 & 8 & 52 & 17.3\% \\
benign\_tumor & 10 & 9 & 38 & 12.7\% \\
cyst & 11 & 10 & 41 & 13.7\% \\
abscess & 12 & 11 & 12 & 4.0\% \\
\bottomrule
\end{tabular}
\end{table}
\textbf{Примечание:} Проценты указаны относительно общего количества изображений (300).
% ============================================================================
% 5. ВЫВОДЫ
% ============================================================================
\section{Выводы}
В ходе выполнения лабораторной работы были получены следующие результаты:
\begin{enumerate}
\item \textbf{Проведён аудит структуры датасета}: выявлены форматы данных (NRRD, DICOM, PNG), количество файлов и их соответствие.
\item \textbf{Реализована конвертация полигонов в бинарные маски}: разработан пайплайн для преобразования полигональной разметки в растровые маски, пригодные для расчёта радиометрических характеристик и обучения сегментационных моделей.
\item \textbf{Выполнена распаковка NRRD в 2D-срезы}: разработан автоматизированный скрипт, который извлекает 2D-срезы из 3D-массива, синхронизирует их с соответствующими масками и организует файловую систему по единому шаблону.
\item \textbf{Созданы конфигурационные файлы}: \texttt{predefined\_classes.txt} (12 классов) и \texttt{data.yaml} -- единая точка управления классами и путями к данным.
\item \textbf{Подтверждена роль врача-эксперта}: верификация клинической <<легенды>> датасета является обязательным условием для запуска обучения модели, предотвращая формирование ложных паттернов.
\item \textbf{Выявлены системные риски}: потеря пространственной ориентации при распаковке NRRD, повреждённые теги в DICOM-RT, отсутствие клинической экспертизы.
\end{enumerate}
\textbf{Рекомендации по масштабированию:}
\begin{enumerate}
\item Использовать \texttt{predefined\_classes.txt} как единый источник истины для всех экспериментов.
\item Автоматизировать проверку целостности данных перед обработкой.
\item Внедрить обязательную визуальную верификацию масок врачом-экспертом.
\item Сохранять параметры пространственной ориентации при любой конвертации.
\end{enumerate}
% ============================================================================
% 6. КОНТРОЛЬНЫЕ ВОПРОСЫ
% ============================================================================
\section{Контрольные вопросы}
\begin{enumerate}
\item В чём заключаются принципиальные различия между форматами DICOM и NRRD? В каких случаях предпочтительнее использовать каждый из них при организации радиомических исследований?
\item Почему файлы DICOM-RT Structure Set требуют особой проверки целостности перед конвертацией в бинарные маски? Какие последствия могут возникнуть при наличии повреждённых тегов?
\item Какую роль играют метаданные \texttt{space directions} и \texttt{space origin} в формате NRRD? Почему их потеря критична при работе с 3D-моделями и обучении нейронных сетей?
\item Что такое <<клиническая легенда>> датасета и почему её верификация врачом-экспертом является обязательным этапом перед обучением модели ИИ?
\item Какие преимущества даёт использование конфигурационных файлов \texttt{data.yaml} и \texttt{predefined\_classes.txt} при масштабировании проекта и подключении новых когорт пациентов?
\item Проанализируйте структуру закреплённого за вами датасета. К какой группе (DICOM или NRRD) он относится? Какие особенности хранения данных и разметки вы выявили?
\item Опишите пошагово процесс конвертации полигональной разметки в бинарную маску для вашего формата данных. Какие библиотеки Python вы использовали и почему?
\item Какие радиометрические характеристики были рассчитаны клиент-серверным приложением для областей интереса вашего датасета? Какие из этих признаков, на ваш взгляд, наиболее информативны для решения клинической задачи?
\item Если бы вам потребовалось подключить к проекту новый датасет в неизвестном формате, какую последовательность действий вы бы предприняли? На каком этапе потребовалось бы привлечение врача-эксперта?
\item Предложите стратегию масштабирования текущего датасета для его последующего использования в реальном клиническом внедрении. Какие дополнительные данные и в каком объёме необходимо привлечь?
\end{enumerate}
% ============================================================================
% 7. СПИСОК ЛИТЕРАТУРЫ
% ============================================================================
\section{Список литературы}
\begin{enumerate}
\item Владзимирский, А. В. Компьютерное зрение в лучевой диагностике: первый этап Московского эксперимента / А. В. Владзимирский, Ю. А. Васильев, К. М. Арзамасов [и др.]. -- 2-е изд. -- Москва : ООО <<Издательские решения>>, 2023. -- 388 с. -- ISBN 978-5-0059-3043-9.
\item Гавриченков, Ю. Д. Системы искусственного интеллекта : учебно-методическое пособие / Ю. Д. Гавриченков, В. Ю. Переверзев. -- Самара : Медицинский университет <<Реавиз>>, 2024. -- 55 с.
\item Поляков, Е. В. Цифровая обработка медицинских изображений : учебное пособие / Е. В. Поляков, В. В. Дмитриева, А. А. Фроня. -- Москва : НИЯУ МИФИ, 2024. -- 100 с.
\item Савченко, Е. Ю. Основы и применение нейронных сетей: теория, практика, инновации : учебное пособие / Е. Ю. Савченко. -- Бишкек : Нео Принт, 2024. -- 116 с.
\item Молодяков, С. А. Применение функций OpenCV в компьютерном зрении (60 примеров на Python) / С. А. Молодяков. -- Санкт-Петербург : Изд-во Политехн. ун-та, 2022. -- 296 с.
\item Jocher, G. Ultralytics YOLOv8 Documentation. -- URL: https://docs.ultralytics.com/ (дата обращения: 21.08.2026).
\item van Griethuysen, J. J. M. Computational Radiomics System to Decode the Radiographic Phenotype / J. J. M. van Griethuysen, A. Fedorov, C. Parmar [et al.] // Cancer Research. -- 2017. -- Vol. 77, No. 21. -- P. e104--e107.
\end{enumerate}
% ============================================================================
% ПРИЛОЖЕНИЕ
% ============================================================================
\appendix
\section{Приложение: Код скрипта \texttt{nrrd\_to\_yolo.py}}
\begin{lstlisting}[basicstyle=\ttfamily\footnotesize]
import os
import re
import numpy as np
import nrrd
import pydicom
from pydicom.uid import ExplicitVRLittleEndian
from pydicom.uid import generate_uid
from pathlib import Path
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
from matplotlib.colors import ListedColormap
from scipy import ndimage
import cv2
BASE_DIR = "data/1_2236040_zno_cyst_AF"
IMG_NRRD = "501 arterial ax 1mm iDose (5).nrrd"
SEG_NRRD = "Segmentation.seg.nrrd"
CT_CMAP = 'gray'
POLY_EPSILON = 0.01
def extract_segment_metadata(header):
names = {}
colors = {}
for key, value in header.items():
if key.startswith('Segment') and key.endswith('_LabelValue'):
match = re.search(r'Segment(\d+)_LabelValue', key)
if match:
seg_idx = int(match.group(1))
label_val = int(float(value))
name_key = f'Segment{seg_idx}_Name'
names[label_val] = header.get(name_key, '')
color_key = f'Segment{seg_idx}_Color'
color_str = header.get(color_key, '')
if color_str:
parts = color_str.strip().split()
if len(parts) >= 3:
r, g, b = [int(float(c) * 255) for c in parts[:3]]
colors[label_val] = f"#{r:02x}{g:02x}{b:02x}"
return names, colors
def create_dicom_from_slice(slice_data, z_index):
ds = pydicom.Dataset()
ds.file_meta = pydicom.Dataset()
ds.file_meta.TransferSyntaxUID = ExplicitVRLittleEndian
ds.file_meta.MediaStorageSOPClassUID = '1.2.840.10008.5.1.4.1.1.2'
ds.file_meta.MediaStorageSOPInstanceUID = generate_uid()
ds.file_meta.ImplementationClassUID = generate_uid()
ds.SOPClassUID = ds.file_meta.MediaStorageSOPClassUID
ds.SOPInstanceUID = ds.file_meta.MediaStorageSOPInstanceUID
ds.PatientName = "ANON^PATIENT"
ds.PatientID = "000000"
ds.StudyDate = "20260602"
ds.Modality = "CT"
ds.InstanceNumber = str(z_index)
ds.Rows, ds.Columns = slice_data.shape
ds.PixelSpacing = [1.0, 1.0]
ds.SliceThickness = 1.0
ds.BitsAllocated = 16
ds.BitsStored = 16
ds.HighBit = 15
ds.PixelRepresentation = 1
ds.ImagePositionPatient = [0.0, 0.0, float(z_index)]
ds.ImageOrientationPatient = [1.0, 0.0, 0.0, 0.0, 1.0, 0.0]
ds.SamplesPerPixel = 1
ds.PhotometricInterpretation = "MONOCHROME2"
ds.PixelData = slice_data.tobytes()
return ds
def extract_polygon(mask_uint8):
contours, _ = cv2.findContours(mask_uint8, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
if not contours:
return None
largest_contour = max(contours, key=cv2.contourArea)
epsilon = POLY_EPSILON * cv2.arcLength(largest_contour, True)
approx = cv2.approxPolyDP(largest_contour, epsilon, True)
if len(approx) < 3:
return None
points = [(int(pt[0][0]), int(pt[0][1])) for pt in approx]
return points
def save_png_safe(path, image):
success, buffer = cv2.imencode('.png', image)
if not success:
raise RuntimeError("cv2.imencode failed")
path.write_bytes(buffer.tobytes())
def main():
base_path = Path(BASE_DIR)
if not base_path.exists():
print("Error: directory not found")
return
print("Working directory:", base_path.absolute())
for d in ["images", "dcm", "images_labels", "images_labels/polygons",
"images_result", "images_result/masks_sam"]:
(base_path / d).mkdir(exist_ok=True)
print("Loading NRRD...")
img_data, _ = nrrd.read(base_path / IMG_NRRD)
seg_data, seg_header = nrrd.read(base_path / SEG_NRRD)
if img_data.shape != seg_data.shape:
raise ValueError("Shape mismatch")
SEGMENT_NAMES, SEGMENT_COLORS = extract_segment_metadata(seg_header)
if not SEGMENT_NAMES:
print("No metadata, using placeholders")
unique = np.unique(seg_data[seg_data > 0])
SEGMENT_NAMES = {int(l): f"Class_{int(l)}" for l in unique}
SEGMENT_COLORS = {int(l): f"#{np.random.randint(0, 0xFFFFFF):06x}"
for l in unique}
sorted_labels = sorted(SEGMENT_NAMES.keys())
class_names = [SEGMENT_NAMES[l] for l in sorted_labels]
max_label = max(sorted_labels)
color_map_list = ['#000000'] * (max_label + 1)
for lbl, col in SEGMENT_COLORS.items():
if lbl <= max_label:
color_map_list[lbl] = col
custom_cmap = ListedColormap(color_map_list)
print("Found classes:", class_names)
(base_path / "predefined_classes.txt").write_text("\n".join(class_names))
W, H, D = img_data.shape
count = 0
mask_count = 0
for z in range(D):
img_slice = img_data[:, :, z].T
seg_slice = seg_data[:, :, z].T
create_dicom_from_slice(img_slice.astype(np.int16), z).save_as(
base_path / "dcm" / f"slice_{z:04d}.dcm")
p_min, p_max = np.percentile(img_slice, [0.5, 99.5])
img_uint8 = (np.clip(img_slice, p_min, p_max) - p_min) / (p_max - p_min + 1e-8) * 255
img_uint8 = img_uint8.astype(np.uint8)
slice_labels = np.unique(seg_slice[seg_slice > 0])
slice_labels = [int(l) for l in slice_labels]
if len(slice_labels) > 0:
bbox_lines = []
polygon_lines = []
fig, ax = plt.subplots(figsize=(img_slice.shape[1]/100,
img_slice.shape[0]/100))
ax.imshow(img_uint8, cmap=CT_CMAP)
ax.imshow(seg_slice, cmap=custom_cmap, alpha=0.4)
H_curr, W_curr = img_slice.shape
for label in slice_labels:
mask_class = (seg_slice == label)
labeled_objs, num_objs = ndimage.label(mask_class,
structure=np.ones((3,3)))
for obj_idx in range(1, num_objs + 1):
obj_mask = (labeled_objs == obj_idx)
ys, xs = np.where(obj_mask)
if len(ys) == 0:
continue
x_min, y_min, x_max, y_max = xs.min(), ys.min(), xs.max(), ys.max()
xc = (x_min + x_max) / 2.0
yc = (y_min + y_max) / 2.0
bw = x_max - x_min
bh = y_max - y_min
yolo_class_id = sorted_labels.index(label)
bbox_lines.append(
f"{yolo_class_id} {xc/W_curr:.6f} {yc/H_curr:.6f} "
f"{bw/W_curr:.6f} {bh/H_curr:.6f}")
obj_mask_uint8 = (obj_mask * 255).astype(np.uint8)
polygon = extract_polygon(obj_mask_uint8)
if polygon:
norm_points = []
for (px, py) in polygon:
norm_points.append(f"{px/W_curr:.6f}")
norm_points.append(f"{py/H_curr:.6f}")
polygon_lines.append(f"{yolo_class_id} " + " ".join(norm_points))
single_mask = np.zeros((H_curr, W_curr), dtype=np.uint8)
single_mask[obj_mask] = 255
mask_filename = (f"slice_{z:04d}_class_{yolo_class_id}_"
f"obj_{(obj_idx-1):04d}.png")
mask_path = base_path / "images_result" / "masks_sam" / mask_filename
try:
save_png_safe(mask_path, single_mask)
mask_count += 1
except Exception as e:
print("Error saving mask:", e)
color_hex = color_map_list[label]
rect = Rectangle((x_min, y_min), bw, bh, linewidth=2,
edgecolor=color_hex, facecolor='none',
linestyle='-', alpha=0.7)
ax.add_patch(rect)
if polygon:
poly_pts = np.array(polygon)
closed_pts = np.vstack([poly_pts, poly_pts[0:1]])
ax.plot(closed_pts[:, 0], closed_pts[:, 1],
color=color_hex, linewidth=2.5,
linestyle='--', alpha=0.9)
(base_path / "images_labels" / f"slice_{z:04d}.txt").write_text(
"\n".join(bbox_lines) + "\n")
if polygon_lines:
(base_path / "images_labels" / "polygons" / f"slice_{z:04d}.txt").write_text(
"\n".join(polygon_lines) + "\n")
plt.imsave(base_path / "images" / f"slice_{z:04d}.png",
img_uint8, cmap=CT_CMAP, format='png')
ax.set_title(f"Slice {z}")
ax.axis('off')
plt.tight_layout(pad=0)
plt.savefig(base_path / "images_result" / f"slice_{z:04d}_preview.png",
dpi=150, bbox_inches='tight')
plt.close(fig)
count += 1
(base_path / "data.yaml").write_text(
f"path: {base_path.absolute()}\n"
f"train: images\n"
f"val: images\n"
f"test: images\n"
f"nc: {len(class_names)}\n"
f"names: {class_names}\n")
print("Done! Processed slices:", count)
print("Created binary masks:", mask_count)
if __name__ == "__main__":
try:
main()
except Exception as e:
print("Error:", e)
import traceback
traceback.print_exc()
\end{lstlisting}
\end{document}