Files

975 lines
47 KiB
TeX
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
% ============================================================================
% ЛАБОРАТОРНАЯ РАБОТА №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}