% ============================================================================ % ЛАБОРАТОРНАЯ РАБОТА №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{Формирование ложных паттернов}: нейросеть может научиться распознавать не патологию, а артефакты сканирования или особенности оборудования (<>). \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{ } \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{ ... } \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}