ИИ-вертебролог на Python: анализ МРТ с Gemini 3 Flash | Гайд 2026 | AiManual
AiManual Logo Ai / Manual.
03 Мар 2026 Гайд

Создаём ИИ-вертебролога на Python: анализ МРТ с Gemini 3 Flash и структурированными промптами

Пошаговый гайд по созданию ИИ для анализа МРТ позвоночника на Python с Gemini 3 Flash. Структурированные промпты, обработка DICOM, GUI на CustomTkinter.

Почему каждый второй офисный работник нуждается в ИИ-вертебрологе?

Представь ситуацию: после 8 часов за монитором спина ноет так, что кажется, позвонки вот-вот сложатся в оригами. Ты идешь на МРТ, получаешь пачку непонятных снимков и ждешь неделю, чтобы врач нашел 15 минут для расшифровки. А что если бы ИИ мог дать предварительный анализ за 30 секунд? Не заменяя врача, а экономя его время для сложных случаев.

Вот где мультимодальные модели вроде Gemini 3 Flash перестают быть игрушкой для генерации мемов и становятся реальным инструментом. Они видят изображения, понимают контекст и могут структурировать ответ. Главная проблема - заставить их говорить на языке медицины, а не философии. Для этого нужны не просто промпты, а инженерные конструкции.

Важно: Этот ИИ - не медицинский диагност. Это помощник для первичного скрининга и структурирования данных. Все заключения должен проверять врач. В некоторых странах (включая РФ на 03.03.2026) использование ИИ для медицинской диагностики требует сертификации.

Архитектура: почему Gemini 3 Flash, а не GPT-5 или Claude?

На 03.03.2026 у Google Gemini 3 Flash - самая сбалансированная модель для мультимодальных задач. Она быстрее и дешевле Pro-версии, но сохраняет способность анализировать изображения с медицинской точностью. В отличие от GPT-5, у Gemini лучше работа со структурированным выводом (JSON), что критично для автоматизации. Claude силен в тексте, но с изображениями медицины у него до сих пор проблемы.

Стек технологий:

  • Gemini 3 Flash через Google AI Python SDK (версия 2.4.0 на 03.2026)
  • CustomTkinter для GUI - выглядит как современное приложение, а не как Windows 98
  • Pydicom и PIL для работы с DICOM-файлами
  • Python-dotenv для хранения API-ключа (никогда не хардкоди ключи!)
💡
Если не знаешь, как работать с Gemini API, посмотри мой гайд "40 практических советов по работе с Gemini 3". Там разобраны все базовые сценарии, включая обработку изображений.

1 Подготовка: получаем API ключ и ставим библиотеки

Первое, что убьет твой проект - неправильная настройка окружения. Не делай так:

# ТАК НЕ ДЕЛАТЬ!
pip install google-generativeai pillow pydicom customtkinter python-dotenv
# И потом мучаться с конфликтами версий

Вместо этого создай виртуальное окружение и зафиксируй версии:

python -m venv venv_vertebrolog
source venv_vertebrolog/bin/activate  # На Windows: venv_vertebrolog\Scripts\activate

pip install google-generativeai==2.4.0
pip install customtkinter==5.2.2
pip install pydicom==2.4.4
pip install pillow==10.3.0
pip install python-dotenv==1.0.1

API ключ получаем в Google AI Studio. Бесплатно дают 60 запросов в минуту, для тестового проекта хватит. Ключ сохраняем в .env файл, который добавляем в .gitignore:

# .env
GEMINI_API_KEY=your_actual_key_here

2 Структура промпта: заставляем Gemini думать как вертебролог

Вот где большинство проектов умирает. Люди пишут "Проанализируй это МРТ" и получают поток сознания про красоту спиралей. Нам нужен структурированный ответ в JSON. Секрет - в системном промпте с четкой ролью и форматом.

💡
Если хочешь глубже понять, как работают системные промпты, прочитай "Системный промпт Gemini 3 Pro: анализ утечки". Там разобраны техники, которые заставляют модель следовать инструкциям.

Правильный промпт выглядит так:

SYSTEM_PROMPT = """
Ты - ассистент вертебролога с 15-летним опытом анализа МРТ позвоночника.
Твоя задача - анализировать предоставленные снимки МРТ и возвращать структурированную оценку.

ИНСТРУКЦИИ:
1. Анализируй только видимые на снимке отделы позвоночника (шейный, грудной, поясничный, крестцовый)
2. Оценивай: состояние межпозвонковых дисков (высота, протрузии, грыжи), состояние тел позвонков, спинномозгового канала
3. Используй стандартную медицинскую терминологию, но избегай панических формулировок
4. Если снимок нечеткий или недостаточно информативный - укажи это
5. Всегда возвращай ответ в следующем JSON-формате:

{
  "vertebral_sections_visible": ["шейный", "поясничный"],
  "findings": [
    {
      "level": "C5-C6",
      "finding_type": "протрузия",
      "size_mm": 2.5,
      "description": "Дорзальная протрузия диска, умеренно суживающая позвоночный канал"
    }
  ],
  "overall_assessment": "Умеренные дегенеративные изменения в шейном отделе",
  "recommendations": ["Консультация невролога", "МРТ контроль через год"],
  "image_quality": "удовлетворительная",
  "confidence_score": 0.85
}

Не добавляй никакого текста вне JSON. Не комментируй. Только чистый JSON.
"""

Почему это работает? Во-первых, мы задаем роль - это не просто ИИ, это эксперт с опытом. Во-вторых, даем конкретные инструкции что анализировать. В-третьих, показываем пример формата. Gemini 3 Flash отлично следует таким инструкциям, если они четкие.

3 Код загрузки и конвертации DICOM: самая скучная часть

МРТ-аппараты выдают DICOM-файлы (.dcm). Это не просто картинки, а структурированные данные с метаинформацией. Но Gemini работает с PNG/JPEG. Значит, нужно конвертировать. Вот как это сделать без потери диагностически значимой информации:

import pydicom
from PIL import Image
import numpy as np
import io

def dicom_to_pil(dicom_path):
    """Конвертируем DICOM в PIL Image с нормализацией контраста"""
    dicom = pydicom.dcmread(dicom_path)
    
    # Извлекаем пиксельные данные
    pixel_array = dicom.pixel_array
    
    # Нормализация контраста для лучшей визуализации
    # Медицинский лайфхак: используем окно центровки и ширины из DICOM, если они есть
    if 'WindowCenter' in dicom and 'WindowWidth' in dicom:
        center = float(dicom.WindowCenter)
        width = float(dicom.WindowWidth)
        low = center - width / 2
        high = center + width / 2
        pixel_array = np.clip(pixel_array, low, high)
    
    # Масштабируем к 0-255
    pixel_array = ((pixel_array - pixel_array.min()) / 
                  (pixel_array.max() - pixel_array.min()) * 255).astype(np.uint8)
    
    # Создаем изображение
    if len(pixel_array.shape) == 2:  # 2D снимок
        img = Image.fromarray(pixel_array, mode='L')
    elif len(pixel_array.shape) == 3:  # RGB снимок
        img = Image.fromarray(pixel_array, mode='RGB')
    else:
        raise ValueError(f"Неизвестная размерность DICOM: {pixel_array.shape}")
    
    # Ресайз для Gemini (слишком большие изображения могут вызвать ошибки)
    img.thumbnail((1024, 1024))
    
    return img

def pil_to_bytes(img):
    """Конвертируем PIL Image в байты для отправки в Gemini"""
    img_byte_arr = io.BytesIO()
    img.save(img_byte_arr, format='PNG')
    return img_byte_arr.getvalue()

Важный нюанс: некоторые DICOM-файлы содержат несколько срезов (серии). В нашем упрощенном примере берем первый срез. В реальном приложении нужно либо выбирать самый репрезентативный, либо анализировать несколько.

💡
Работа с медицинскими изображениями - это отдельная наука. Если хочешь сделать более продвинутую систему, посмотри статью про магнитные поля и WebGPU. Там есть техники обработки 3D медицинских данных.

4 Интеграция с Gemini API: отправляем снимок и парсим JSON

Теперь самое интересное - отправка изображения в модель. На 03.03.2026 Gemini API поддерживает как base64, так и прямую загрузку файлов. Мы используем второй вариант, он проще.

import google.generativeai as genai
from dotenv import load_dotenv
import os
import json

load_dotenv()

class VertebrologAI:
    def __init__(self):
        api_key = os.getenv('GEMINI_API_KEY')
        if not api_key:
            raise ValueError("API ключ не найден. Создай .env файл с GEMINI_API_KEY")
        
        genai.configure(api_key=api_key)
        self.model = genai.GenerativeModel(
            'gemini-3.0-flash',
            system_instruction=SYSTEM_PROMPT  # Берем из предыдущего шага
        )
    
    def analyze_mri(self, image_bytes):
        """Анализируем МРТ и возвращаем структурированные данные"""
        try:
            # Создаем объект изображения для Gemini
            image_part = {
                "mime_type": "image/png",
                "data": image_bytes
            }
            
            # Отправляем запрос с изображением и текстовым промптом
            response = self.model.generate_content([
                "Проанализируй это МРТ позвоночника и верни результат в указанном формате.",
                image_part
            ])
            
            # Извлекаем текст ответа
            response_text = response.text
            
            # Пытаемся найти JSON в ответе
            # Иногда Gemini добавляет лишний текст, даже если просили только JSON
            start_idx = response_text.find('{')
            end_idx = response_text.rfind('}') + 1
            
            if start_idx == -1 or end_idx == 0:
                raise ValueError("Gemini не вернул JSON в ответе")
            
            json_str = response_text[start_idx:end_idx]
            
            # Парсим JSON
            result = json.loads(json_str)
            
            # Добавляем сырой ответ для отладки
            result['raw_response'] = response_text
            
            return result
            
        except json.JSONDecodeError as e:
            print(f"Ошибка парсинга JSON: {e}")
            print(f"Ответ Gemini: {response_text}")
            return {"error": "Ошибка парсинка ответа", "raw_response": response_text}
        except Exception as e:
            print(f"Ошибка при анализе: {e}")
            return {"error": str(e)}

Обрати внимание на обработку ошибок. Gemini иногда возвращает JSON с дополнительным текстом вокруг ("Вот ваш JSON: {...}"). Наш код это учитывает, вырезая сам JSON.

Ловушка: Не используй response.parts или response.candidates без проверки. В Gemini 3 Flash структура ответа изменилась по сравнению с Gemini 1.0. Всегда проверяй response.text в первую очередь.

5 GUI на CustomTkinter: делаем интерфейс, который не стыдно показать

Текстовый интерфейс - это 1990-е. Современные медицинские ассистенты должны выглядеть профессионально. CustomTkinter дает Material Design вид без изучения Qt.

import customtkinter as ctk
from tkinter import filedialog
import threading

class VertebrologApp:
    def __init__(self):
        ctk.set_appearance_mode("light")
        ctk.set_default_color_theme("blue")
        
        self.window = ctk.CTk()
        self.window.title("ИИ-вертебролог v1.0 (03.2026)")
        self.window.geometry("900x700")
        
        self.ai = VertebrologAI()
        self.current_image = None
        
        self.setup_ui()
        
    def setup_ui(self):
        # Панель загрузки
        self.load_frame = ctk.CTkFrame(self.window)
        self.load_frame.pack(pady=20, padx=20, fill="x")
        
        ctk.CTkLabel(self.load_frame, text="Загрузите DICOM или PNG файл МРТ:", 
                     font=("Arial", 16)).pack(pady=10)
        
        self.load_btn = ctk.CTkButton(self.load_frame, text="Выбрать файл",
                                     command=self.load_file)
        self.load_btn.pack(pady=10)
        
        # Область изображения
        self.image_frame = ctk.CTkFrame(self.window)
        self.image_frame.pack(pady=10, padx=20, fill="both", expand=True)
        
        self.image_label = ctk.CTkLabel(self.image_frame, text="")
        self.image_label.pack(pady=20)
        
        # Кнопка анализа
        self.analyze_btn = ctk.CTkButton(self.window, text="Анализировать МРТ",
                                        command=self.start_analysis,
                                        state="disabled")
        self.analyze_btn.pack(pady=10)
        
        # Прогресс бар
        self.progress = ctk.CTkProgressBar(self.window)
        self.progress.pack(pady=5, padx=20, fill="x")
        self.progress.set(0)
        
        # Результаты
        self.result_text = ctk.CTkTextbox(self.window, height=200)
        self.result_text.pack(pady=20, padx=20, fill="both", expand=True)
        
    def load_file(self):
        file_path = filedialog.askopenfilename(
            filetypes=[("Медицинские изображения", "*.dcm *.png *.jpg")]
        )
        if file_path:
            if file_path.endswith('.dcm'):
                img = dicom_to_pil(file_path)
            else:
                img = Image.open(file_path)
            
            # Показываем превью
            img.thumbnail((400, 400))
            ctk_img = ctk.CTkImage(light_image=img, size=img.size)
            self.image_label.configure(image=ctk_img, text="")
            
            # Сохраняем для анализа
            self.current_image = img
            self.analyze_btn.configure(state="normal")
            
    def start_analysis(self):
        # Запускаем в отдельном потоке, чтобы GUI не зависал
        self.progress.set(0.5)
        self.analyze_btn.configure(state="disabled")
        self.result_text.delete("1.0", "end")
        self.result_text.insert("1.0", "Анализ начался...\n")
        
        thread = threading.Thread(target=self.perform_analysis)
        thread.start()
        
    def perform_analysis(self):
        try:
            image_bytes = pil_to_bytes(self.current_image)
            result = self.ai.analyze_mri(image_bytes)
            
            # Обновляем GUI из главного потока
            self.window.after(0, self.display_results, result)
            
        except Exception as e:
            self.window.after(0, self.display_error, str(e))
        finally:
            self.window.after(0, self.analysis_complete)
            
    def display_results(self, result):
        self.progress.set(1.0)
        
        text = ""
        if "error" in result:
            text = f"Ошибка: {result['error']}\n\n"
            text += f"Сырой ответ:\n{result.get('raw_response', 'Нет')}"
        else:
            text += f"ОТДЕЛЫ: {', '.join(result['vertebral_sections_visible'])}\n\n"
            text += f"ОБЩАЯ ОЦЕНКА: {result['overall_assessment']}\n\n"
            text += "НАХОДКИ:\n"
            for finding in result['findings']:
                text += f"- {finding['level']}: {finding['finding_type']} ({finding['size_mm']} мм)\n"
                text += f"  {finding['description']}\n"
            text += f"\nРЕКОМЕНДАЦИИ:\n"
            for rec in result['recommendations']:
                text += f"- {rec}\n"
            text += f"\nКачество снимка: {result['image_quality']}"
            text += f"\nУверенность модели: {result['confidence_score']}"
            
        self.result_text.delete("1.0", "end")
        self.result_text.insert("1.0", text)
        
    def display_error(self, error):
        self.result_text.delete("1.0", "end")
        self.result_text.insert("1.0", f"Критическая ошибка:\n{error}")
        
    def analysis_complete(self):
        self.progress.set(0)
        self.analyze_btn.configure(state="normal")
        
    def run(self):
        self.window.mainloop()

if __name__ == "__main__":
    app = VertebrologApp()
    app.run()

Вот и все. Приложение готово. Оно загружает DICOM, конвертирует, отправляет в Gemini, получает структурированный ответ и показывает его в читаемом виде.

Где этот проект падает и как его укреплять

Теперь о грустном. Базовый вариант работает, но в продакшене он разобьется о реальность. Вот главные проблемы и их решения:

1. Gemini ошибается в размерах и локализациях

Модель может сказать "грыжа 5 мм", когда на самом деле 3 мм. Решение - калибровка. Собери датасет из 100-200 реальных МРТ с экспертной разметкой и используй few-shot learning. В промпт добавь примеры правильных измерений.

2. DICOM - это не один снимок, а серия

Мы анализируем один срез, но патология может быть видна только на соседних. Решение - загружать серию срезов (например, 5 ключевых) и анализировать их вместе. Gemini 3 Flash поддерживает до 16 изображений в одном запросе.

💡
Для работы с последовательностями изображений можешь использовать техники из статьи про Step 3.5 Flash. Там описаны методы анализа временных рядов и последовательностей, которые можно адаптировать для серий МРТ.

3. Медленная работа с большими файлами

DICOM-файлы весят сотни мегабайт. Конвертация и отправка занимают время. Решение - препроцессинг на стороне клиента. Используй библиотеку like SimpleITK для быстрого извлечения ключевых срезов и сжатия без потери качества.

4. Юридические риски

На 03.03.2026 в ЕС уже действуют строгие правила для медицинских ИИ (EU AI Act). В РФ готовится аналогичный закон. Решение - добавлять дисклеймеры, вести лог всех анализов, интегрировать с EMR (электронными медкартами) только через сертифицированные шлюзы.

Вопросы, которые тебе зададут на первом же демо

Вопрос Правильный ответ Неправильный ответ
Это заменяет врача? Нет, это инструмент для первичного скрининга. Все заключения проверяет человек. Да, ИИ точнее человека
На каких данных обучался? Gemini обучалась на публичных медицинских датасетах, но мы дообучаем на своих данных с согласия пациентов. На всем интернете, включая ваши МРТ (это вызовет панику)
Что с конфиденциальностью? Изображения анонимизируются, метаданные удаляются, данные не сохраняются после анализа. Мы храним все на своих серверах в незашифрованном виде
Точность? На тестовой выборке 92% совпадения с экспертами по обнаружению грыж, но для каждой патологии разная. 100%, никогда не ошибается

Что делать дальше с этим проектом

Если ты дочитал до этого места, значит тебе действительно интересно. Вот три направления для развития:

  1. Добавь сравнение с предыдущими МРТ - самый мощный фича. Загрузи два снимка с разницей в год, и ИИ покажет прогрессию патологии.
  2. Интегрируй генерацию текста для врача - на основе структурированных данных автоматически создавай текст для медицинского заключения.
  3. Сделай веб-версию на Streamlit - чтобы не устанавливать Python, врачи могли загружать снимки через браузер.

Самое главное - не останавливайся на демо-версии. Медицинский ИИ - это не про хайп, а про ежедневную работу над точностью и надежностью. Каждый улучшенный промпт, каждый новый пример в датасете - это потенциально сэкономленные часы работы врача и более ранняя диагностика для пациента.

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

Подписаться на канал