Обучение нейросети физике дефектов плёнки для реставрации видео | AiManual
AiManual Logo Ai / Manual.
02 Янв 2026 Гайд

Как я учил нейросеть физике дефектов плёнки вместо простого размытия

Практический гайд по обучению Real-ESRGAN для реставрации видео с учётом физики дефектов плёнки вместо стандартного размытия шума.

Почему размытие — это тупик

Все современные инструменты для реставрации видео делают одно и то же — они размывают шум. Topaz AI, DAIN, даже стандартный Real-ESRGAN. Берут кадр, находят пиксели, которые выглядят "неправильно", и заменяют их усреднёнными значениями соседей. Результат? Чистая, гладкая картинка, в которой нет ни царапин, ни зернистости, ни шума. И нет души.

Плёнка — это не цифровой шум. Это физический объект с историей. Каждая царапина на ней появилась потому, что плёнку протянули через грязный проектор. Каждая пылинка прилипла в конкретном месте. Зернистость — это не случайный артефакт, а структура эмульсии. Когда ты просто размываешь всё это, ты стираешь не только дефекты, но и текстуру, объём, материальность изображения.

Стандартные модели учатся на парах "грязное-чистое" изображение. Но "чистое" в датасетах — это обычно цифровая картинка без дефектов. Модель не понимает, что удалять, а что сохранять. Она просто заучивает: шум = плохо, гладкость = хорошо.

Физика вместо статистики

Мой подход начинался с простого вопроса: а что, если вместо того, чтобы учить нейросеть "убирать шум", научить её понимать, как возникают дефекты? Не статистически, а физически.

Царапина на плёнке — это не случайные пиксели. Это длинная тонкая линия, которая:

  • Имеет направление (обычно горизонтальное, потому что плёнку протягивают горизонтально)
  • Имеет переменную глубину — в начале царапина глубже, в конце мельче
  • Создает не просто тёмную линию, а комплексный артефакт с отражениями, преломлениями света
  • Может "рваться" на кадрах, где плёнка временно отрывалась от головки проектора

Пыль и волосы — это трёхмерные объекты, которые отбрасывают тени. Они не просто "белые точки", а структуры с объёмом. Когда проекторный луч проходит через пылинку на поверхности плёнки, он рассеивается, создавая характерное размытое пятно с ядром и ореолом.

💡
Ключевая идея: вместо обучения модели "что удалять", мы обучаем её "как выглядит дефект в контексте физического носителя". Модель должна понимать разницу между царапиной на плёнке и, например, тёмной линией на костюме актёра.

Собираем датасет, который не существует

Первая проблема: готовых датасетов с помеченными физическими дефектами плёнки нет. Вообще. Те датасеты, что используются для обучения Real-ESRGAN и подобных моделей, содержат синтетический шум — Gaussian noise, JPEG artifacts, random pixels. Никакой физики.

Пришлось создавать свой. Вот как это работает:

1 Находим чистые цифровые источники

Берём современные фильмы в 4K, снятые цифрой. Или сканы плёнки, сделанные на профессиональном оборудовании с последующей ручной реставрацией. Важно: исходник должен быть максимально чистым. Я использовал:

  • 4K рипы современных фильмов (без сжатия, если возможно)
  • Кадры из открытых датасетов DIV2K
  • Собственные фотографии высокого разрешения

2 Синтезируем дефекты физически

Вот где начинается магия. Вместо random noise мы создаём дефекты, которые ведут себя как реальные:

import numpy as np
import cv2
from scipy import ndimage

def add_film_scratch(image, length=100, depth_variation=0.3):
    """Добавляет царапину с физическими свойствами"""
    h, w = image.shape[:2]
    
    # Начальная точка (случайная по высоте)
    start_y = np.random.randint(0, h)
    
    # Царапина не идеально прямая - есть микроколебания
    scratch_width = np.random.randint(1, 3)
    
    # Глубина меняется вдоль царапины
    for i in range(length):
        if i < w:
            # Вертикальное колебание (дрожание руки/проектора)
            current_y = start_y + int(np.sin(i/10) * 2)
            
            # Глубина уменьшается к концу
            depth = 1.0 - (i/length) * depth_variation
            
            # Царапина создаёт не просто чёрную линию, а комплексный артефакт
            for dy in range(-scratch_width, scratch_width+1):
                if 0 <= current_y+dy < h:
                    # Уменьшаем интенсивность пикселей
                    image[current_y+dy, i] = image[current_y+dy, i] * (0.3 * depth)
                    
                    # Добавляем "края" царапины (отражения)
                    if abs(dy) == scratch_width:
                        image[current_y+dy, i] = image[current_y+dy, i] * 1.2
    
    return image

def add_dust_particle(image, x, y, size=3):
    """Добавляет пылинку с объёмом и ореолом"""
    # Ядро пылинки - почти белое
    cv2.circle(image, (x, y), size, (230, 230, 230), -1)
    
    # Ореол рассеяния (размытие только вокруг пылинки)
    kernel_size = size * 2 + 1
    kernel = np.ones((kernel_size, kernel_size), np.float32) / (kernel_size**2)
    
    # Применяем размытие только к области вокруг пылинки
    roi = image[y-size*2:y+size*2, x-size*2:x+size*2]
    if roi.shape[0] > 0 and roi.shape[1] > 0:
        blurred = cv2.filter2D(roi, -1, kernel)
        # Смешиваем с оригиналом для создания ореола
        image[y-size*2:y+size*2, x-size*2:x+size*2] = cv2.addWeighted(
            roi, 0.7, blurred, 0.3, 0
        )
    
    return image

Это упрощённый код, но он показывает принцип: мы не добавляем случайный шум. Мы создаём структуры, которые ведут себя как реальные физические объекты.

Важный момент: дефекты должны добавляться с учётом содержимого кадра. Царапина, которая проходит через лицо персонажа и через фон, должна выглядеть по-разному. На лице она будет взаимодействовать с текстурой кожи, на фоне — с текстурой стен или неба.

3 Добавляем временную согласованность

В видео дефекты движутся! Пылинка не прыгает по кадру случайным образом. Если она попала на плёнку, она остаётся в примерно той же позиции несколько кадров, потом может сместиться или исчезнуть. Царапины вообще статичны относительно плёнки, но движутся относительно кадра (потому что плёнка движется).

Для обучения я создавал последовательности из 5-10 кадров с согласованными дефектами:

def add_temporal_defects(frames):
    """Добавляет дефекты, согласованные во времени"""
    # Пылинки появляются и исчезают постепенно
    dust_particles = []
    for _ in range(np.random.randint(5, 15)):
        x = np.random.randint(50, frames[0].shape[1]-50)
        y = np.random.randint(50, frames[0].shape[0]-50)
        lifetime = np.random.randint(3, 10)  # Кадров жизни
        dust_particles.append({
            'x': x, 'y': y, 
            'lifetime': lifetime,
            'age': 0,
            'size': np.random.randint(2, 5)
        })
    
    # Царапины статичны относительно плёнки, но движутся по кадру
    scratch_start_x = np.random.randint(0, frames[0].shape[1]//2)
    scratch_speed = np.random.randint(1, 3)  # Пикселей за кадр
    
    for i, frame in enumerate(frames):
        # Добавляем пылинки
        for dust in dust_particles[:]:
            if dust['age'] < dust['lifetime']:
                # Пылинка становится ярче, потом тускнеет
                intensity = 1.0 - abs(dust['age'] - dust['lifetime']/2) / (dust['lifetime']/2)
                frame = add_dust_particle(frame, dust['x'], dust['y'], dust['size'])
                dust['age'] += 1
            else:
                dust_particles.remove(dust)
        
        # Добавляем движущуюся царапину
        current_x = scratch_start_x + (i * scratch_speed)
        if current_x < frame.shape[1] - 100:
            frame = add_film_scratch(frame, length=100, start_x=current_x)
        
        frames[i] = frame
    
    return frames

Модифицируем Real-ESRGAN для понимания физики

Стандартная архитектура Real-ESRGAN не предназначена для работы с временными последовательностями или понимания типов дефектов. Пришлось модифицировать.

Основные изменения:

Компонент Стандартный Моя версия
Входные данные Один кадр 3 последовательных кадра
Loss функция L1 + Perceptual + GAN + Temporal consistency loss
Архитектура U-Net like + Attention на дефектах

Temporal consistency loss — это ключевое дополнение. Она штрафует модель за то, что дефекты исчезают или появляются скачкообразно между кадрами:

class TemporalConsistencyLoss(nn.Module):
    def __init__(self):
        super().__init__()
        
    def forward(self, output_frames):
        """output_frames: [batch, frames, C, H, W]"""
        loss = 0
        
        # Сравниваем соседние кадры
        for t in range(output_frames.shape[1] - 1):
            frame_diff = torch.abs(output_frames[:, t] - output_frames[:, t+1])
            
            # Штрафуем большие изменения в низкочастотных компонентах
            # (дефекты должны исчезать постепенно)
            loss += torch.mean(frame_diff)
            
            # Дополнительно: штраф за "мерцание" в высоких частотах
            frame_t_fft = torch.fft.fft2(output_frames[:, t])
            frame_t1_fft = torch.fft.fft2(output_frames[:, t+1])
            freq_diff = torch.abs(frame_t_fft - frame_t1_fft)
            
            # Высокие частоты (детали) могут меняться больше
            # Низкие частоты (структура) должны быть стабильны
            h, w = output_frames.shape[-2:]
            freq_mask = torch.zeros((h, w))
            freq_mask[h//4:3*h//4, w//4:3*w//4] = 1  # Низкие частоты в центре
            
            loss += torch.mean(freq_diff * freq_mask.to(output_frames.device))
            
        return loss / (output_frames.shape[1] - 1)

Обучение: боль, терпение и прорывы

Обучение такой модели — это не просто запустить скрипт и ждать. Стандартный подход с фиксированным learning rate и базовыми аугментациями не работает.

1 Фаза 1: Предобучение на синтетике

Первые 50k итераций модель учится на полностью синтетических данных. Цель — понять базовые паттерны дефектов. Learning rate высокий (1e-4), батч маленький (4-8).

Проблема, с которой столкнулся сразу: модель быстро научилась просто размывать всё. Решение — добавить perceptual loss с большим весом и использовать более сложные дефекты.

2 Фаза 2: Добавление реальных данных

После 50k итераций начинаю подмешивать реальные кадры со старых плёнок. Но не как есть — а с парными "чистыми" кадрами, которые я вручную реставрировал для небольшого набора.

Здесь learning rate снижаю до 5e-5. Батч уменьшаю до 2-4 (память GPU).

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

3 Фаза 3: Тонкая настройка на конкретных типах дефектов

Последние 10k итераций — обучение на конкретных типах плёнок. Например, отдельно на плёнках с сильными царапинами, отдельно на плёнках с пылью и волосами.

Learning rate здесь 1e-6, почти микроскопический. Цель — не переучить модель, а адаптировать её к специфическим паттернам.

Результаты: что получилось, а что нет

После двух месяцев обучения (на RTX 4090, около 200 часов чистого времени GPU) модель показала интересные результаты.

Что работает хорошо:

  • Царапины удаляются физически корректно — не просто замазываются, а "залечиваются" с учётом текстуры под ними. Царапина на лице заполняется текстурой кожи, на одежде — текстурой ткани.
  • Пыль и волосы — модель понимает, что это отдельные объекты, а не часть изображения. Удаляет их, но сохраняет фон под ними.
  • Временная стабильность — дефекты исчезают постепенно, нет мерцания или скачков.

Что не работает или работает плохо:

  • Сильные повреждения — если половина кадра разрушена, модель не может восстановить информацию, которой нет. Она пытается, но результат выглядит как галлюцинация.
  • Специфические артефакты — химические повреждения, плесень на плёнке, цветовые искажения от выцветания. Для них нужна отдельная тренировка.
  • Вычислительная сложность — обработка видео в реальном времени невозможна. Один кадр 1080p → 4K занимает около 2-3 секунд на RTX 4090.

Практическое применение: как использовать модель

Если вы хотите попробовать подобный подход, вот минимальный рабочий конвейер:

# 1. Подготовка среды
conda create -n film-restoration python=3.9
conda activate film-restoration
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
pip install opencv-python numpy scipy tqdm

# 2. Клонирование и настройка Real-ESRGAN
git clone https://github.com/xinntao/Real-ESRGAN.git
cd Real-ESRGAN
pip install -r requirements.txt

# 3. Подготовка датасета
# Папка train должна содержать подпапки:
# train/clean/ - чистые изображения
# train/degraded/ - те же изображения с добавленными дефектами
# Важно: имена файлов должны совпадать!

# 4. Запуск обучения с модифицированной архитектурой
python basicsr/train.py -opt options/train/train_film_restoration.yml

Конфигурационный файл train_film_restoration.yml должен содержать ключевые изменения:

# Увеличиваем размер патча для захвата контекста дефектов
dataset:
  train:
    name: PairedImageDataset
    dataroot_gt: ./datasets/train/clean
    dataroot_lq: ./datasets/train/degraded
    io_backend:
      type: disk
    gt_size: 512  # Больше, чем стандартные 256
    
# Модифицированная архитектура с temporal attention
network_g:
  type: FilmRestorationNet
  num_in_ch: 9  # 3 кадра × 3 канала
  num_out_ch: 3
  num_feat: 64
  num_block: 23
  num_grow_ch: 32
  temporal_attention: true
  
# Дополнительные loss функции
losses:
  - type: L1Loss
    weight: 1.0
  - type: PerceptualLoss
    layer_weights:
      'conv5_4': 1.0
    weight: 1.0
  - type: TemporalConsistencyLoss
    weight: 0.5  # Новый loss!

Ошибки, которые стоит избегать

За два месяца я наступил на все возможные грабли. Вот топ-5 ошибок, которые сведут на нет все усилия:

  1. Использовать только синтетические данные — модель научится идеально удалять синтетические дефекты и беспомощна перед реальными.
  2. Не учитывать временную согласованность — получите мерцающее видео, где дефекты прыгают по кадру.
  3. Обучать на однотипных кадрах — если все обучающие кадры — это лица крупным планом, модель не справится с пейзажами или динамичными сценами.
  4. Игнорировать perceptual loss — без него модель будет создавать технически чистые, но "пластиковые" изображения.
  5. Пытаться обрабатывать целое видео за раз — памяти не хватит. Нужно разбивать на сцены и обрабатывать по частям.

Что дальше? Будущее физически-осознанной реставрации

Мой эксперимент показал, что подход работает. Но это только начало. Вот что можно улучшить:

  • Diffusion модели вместо GAN — современные diffusion модели дают лучший контроль над процессом восстановления. Можно буквально "указывать", какие области нужно исправить.
  • Мультимодальное обучение — использовать не только изображение, но и звук (характерный треск при царапинах), метаданные (тип плёнки, год производства).
  • Интерактивная реставрация — модель, которая предлагает несколько вариантов восстановления и позволяет пользователю выбрать наиболее подходящий.

Самое интересное — это возможность создания специализированных моделей для разных типов плёнок. Kodak Tri-X 1960-х годов стареет иначе, чем Fujifilm 2000-х. Царапины на нитратной плёнке (опасной, легковоспламеняющейся) выглядят иначе, чем на безопасной ацетатной основе.

💡
Если вы работаете с локальными LLM для других задач (например, для машинного перевода или анализа кода), вам может быть интересна статья про сравнение локальных LLM с традиционными методами. Там те же принципы — специализация вместо универсальности.

Физически-осознанная реставрация — это не про то, чтобы сделать картинку чище. Это про то, чтобы понять историю носителя и уважительно её сохранить. Убрать повреждения, но оставить характер. Как хороший реставратор картины — удаляет грязь и плесень, но не переписывает мазки художника.

И да, это в тысячу раз сложнее, чем запустить Topaz AI. Но когда видишь, как на кадрах 1950-х годов постепенно исчезают царапины, но остаётся зернистость и фактура плёнки — понимаешь, что оно того стоит.