Real-ESRGANによる動画品質改善、顔復元(ソースコードと実行結果)


画質改善前

画質改善後

Python開発環境,ライブラリ類

ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.htmlで詳しく解説しているので、必要に応じて参照してください。

Python 3.12 のインストール

インストール済みの場合は実行不要。

管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要である。

REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul

関連する外部ページ

Python の公式ページ: https://www.python.org/

AI エディタ Windsurf のインストール

Pythonプログラムの編集・実行には、AI エディタの利用を推奨する。ここでは,Windsurfのインストールを説明する。

管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行して、Windsurfをシステム全体にインストールする。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要となる。

winget install --scope machine Codeium.Windsurf -e --silent

関連する外部ページ

Windsurf の公式ページ: https://windsurf.com/

必要なライブラリのインストール

コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する


pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
pip install basicsr opencv-python pillow numpy requests scikit-image

Real-ESRGAN動画品質改善、顔復元プログラム

AIの基本的な能力と動画編集応用

AIの基本的な能力の1つは、既存のデータから学習したパターンや特徴を理解し、新しい表現を生成することである。AIによる画像・動画処理技術の発展により、専門的な技術や経験がなくても、効率的な品質改善処理が可能となった。

本プログラムの技術的位置づけ

本プログラムは、Real-ESRGAN(Real-World Enhanced Super-Resolution Generative Adversarial Network)技術を用いた動画品質改善システムである。Real-ESRGANは、実世界の劣化画像に対応するため純粋な合成データで訓練された超解像度技術である。

プログラムは以下の技術を使用している。

参考文献

[1] X. Wang, L. Xie, C. Dong, Y. Shan, "Real-ESRGAN: Training Real-World Blind Super-Resolution with Pure Synthetic Data," in Proc. IEEE/CVF International Conference on Computer Vision Workshops (ICCVW), 2021, pp. 1905-1914.

ソースコード


# プログラム名: Real-ESRGAN動画品質改善・顔復元プログラム
# 特徴技術名: Real-ESRGAN (Real-World Enhanced Super-Resolution Generative Adversarial Network)
# 出典: Wang, X., Xie, L., Dong, C., & Shan, Y. (2021). Real-ESRGAN: Training Real-World Blind Super-Resolution with Pure Synthetic Data. In Proceedings of the IEEE/CVF International Conference on Computer Vision (pp. 1905-1914).
# 特徴機能: 実世界劣化に対応した高品質超解像度化。High-order degradation modelingにより、ノイズ、ぼけ、JPEG圧縮アーティファクト等の複雑な劣化を考慮した超解像度化を実現
# 学習済みモデル: RealESRGAN_x4plus(汎用4倍超解像度、23 RRDB構造、実写画像向け)URL: https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth、RealESRGAN_x4plus_anime_6B(アニメ特化、6 RRDB構造、アニメ画像向け)URL: https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth
# 方式設計:
#   - 関連利用技術:
#     * BasicSR(画像復元ツールボックス)- Real-ESRGANの基盤フレームワーク、RRDBNetアーキテクチャ提供
#     * OpenCV(コンピュータビジョンライブラリ)- 動画読み込み、フレーム処理、動画出力
#     * PIL(画像処理ライブラリ)- RGB/BGR色空間変換とNumPy配列との相互変換
#     * FFmpeg(マルチメディア処理ツール)- 動画音声の抽出・結合
#     * scikit-image(画像品質評価)- PSNR・SSIM品質指標計算
#   - 入力と出力: 入力: 動画(ユーザは「0:動画ファイル,1:カメラ,2:サンプル動画」のメニューで選択.0:動画ファイルの場合はtkinterでファイル選択.1の場合はOpenCVでカメラが開く.2の場合はhttps://github.com/opencv/opencv/blob/master/samples/data/vtest.aviを使用)、出力: 処理結果が画像化できる場合にはOpenCV画面でリアルタイムに表示.OpenCV画面内に処理結果をテキストで表示.さらに,1秒間隔で,print()で処理結果を表示.プログラム終了時にprint()で表示した処理結果をresult.txtファイルに保存し,「result.txtに保存」したことをprint()で表示.
#   - 処理手順:
#     1. 入力動画から音声を抽出(FFmpeg利用可能時)
#     2. 動画をフレーム単位で読み込み
#     3. 各フレームをReal-ESRGANで超解像度化
#     4. 処理済みフレームをOpenCV画面に表示
#     5. 元の音声と超解像度化動画を結合(FFmpeg利用可能時)
#     6. 品質評価指標(PSNR・SSIM)を計算
#   - 前処理、後処理:
#     * 色空間変換(BGR→RGB→BGR)によるReal-ESRGAN適合性確保
#     * タイリング処理による大画像対応とVRAM制限回避
#     * outscaleパラメータによるLANCZOS4後処理リサイズ
#   - 追加処理:
#     * 半精度推論(fp16)による推論効率化
#     * GPU/CPUフォールバック機能による安定性確保
#     * システム環境に応じた設定自動最適化
#   - 調整を必要とする設定値:
#     * SCALE_FACTOR(2または4、解像度向上倍率、高倍率ほど処理時間増加)
#     * MODEL_NAME(RealESRGAN_x4plus/RealESRGAN_x4plus_anime_6B、画像特性による選択)
#     * TILE_SIZE(自動最適化、VRAM容量に応じた調整、0で無効化)
# 将来方策: SCALE_FACTORの自動決定のため、入力動画の最初の数フレームで複数のスケール(2,4)を試行し、PSNR/SSIM値が最も高いスケールを自動選択する機能
# その他の重要事項: FFmpegが必要(音声保持機能用)。Windows環境での動作を前提とし、Linux/macOS固有機能は使用しない
# 前準備: pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
#   pip install basicsr opencv-python pillow numpy requests scikit-image

import torch
import cv2
import numpy as np
import os
import requests
import subprocess
from PIL import Image
from pathlib import Path
from skimage.metrics import structural_similarity as ssim
from skimage.metrics import peak_signal_noise_ratio as psnr
import warnings
import tkinter as tk
from tkinter import filedialog
import urllib.request
import time
warnings.filterwarnings('ignore')

# ファイル名・パス関連の定数
WEIGHTS_DIR = 'weights'
RESULT_FILE = 'result.txt'
TEMP_AUDIO_FILE = 'temp_audio.wav'
TEMP_VIDEO_FILE = 'temp_enhanced.mp4'
OUTPUT_VIDEO_FILE = 'enhanced_output.mp4'
SAMPLE_VIDEO_FILENAME = 'vtest.avi'

# 表示関連の定数
FONT_SIZE = 1
FONT_COLOR = (0, 255, 0)
FONT_THICKNESS = 2
TEXT_POSITION = (10, 30)

# 調整可能な設定値
SCALE_FACTOR = 4  # 拡大倍率(2または4を推奨)
MODEL_NAME = 'RealESRGAN_x4plus'  # モデル名("RealESRGAN_x4plus"または"RealESRGAN_x4plus_anime_6B")

# プログラム開始時の説明
print('=== Real-ESRGAN動画品質改善プログラム ===')
print('このプログラムは、Real-ESRGANを使用して動画の超解像度化を行います。')
print('ノイズ除去、ぼけ改善、JPEG圧縮アーティファクト除去などの機能があります。')
print('\n操作方法:')
print('- 動画処理中はqキーで終了できます')
print('- 処理結果はリアルタイムでOpenCV画面に表示されます')
print('- 処理完了後、品質評価結果がresult.txtに保存されます\n')

# FFmpeg利用可能性チェック
def check_ffmpeg():
    try:
        subprocess.run(['ffmpeg', '-version'], capture_output=True, check=True)
        return True
    except (subprocess.CalledProcessError, FileNotFoundError):
        return False

FFMPEG_AVAILABLE = check_ffmpeg()
if not FFMPEG_AVAILABLE:
    print('注意: FFmpegが見つかりません。音声は保持されません。')
    print('音声保持機能を使用するには、FFmpegをインストールしてください: https://ffmpeg.org/download.html\n')

# システム環境に応じた設定の自動最適化
if torch.cuda.is_available():
    gpu_memory = torch.cuda.get_device_properties(0).total_memory / 1024**3
    if gpu_memory >= 8:
        TILE_SIZE = 512
        USE_HALF = True
    elif gpu_memory >= 4:
        TILE_SIZE = 256
        USE_HALF = True
    else:
        TILE_SIZE = 128
        USE_HALF = False
else:
    TILE_SIZE = 64
    USE_HALF = False

# モデルURL設定
MODEL_URLS = {
    'RealESRGAN_x4plus': 'https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth',
    'RealESRGAN_x4plus_anime_6B': 'https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth'
}

# BasicSR代替実装用の関数
def create_rrdbnet_model(num_block):
    """BasicSRが利用できない場合のRRDBNetモデル作成"""
    import torch.nn as nn
    import torch.nn.functional as F

    class ResidualDenseBlock(nn.Module):
        def __init__(self, num_feat=64, num_grow_ch=32):
            super(ResidualDenseBlock, self).__init__()
            self.conv1 = nn.Conv2d(num_feat, num_grow_ch, 3, 1, 1)
            self.conv2 = nn.Conv2d(num_feat + num_grow_ch, num_grow_ch, 3, 1, 1)
            self.conv3 = nn.Conv2d(num_feat + 2 * num_grow_ch, num_grow_ch, 3, 1, 1)
            self.conv4 = nn.Conv2d(num_feat + 3 * num_grow_ch, num_grow_ch, 3, 1, 1)
            self.conv5 = nn.Conv2d(num_feat + 4 * num_grow_ch, num_feat, 3, 1, 1)
            self.lrelu = nn.LeakyReLU(negative_slope=0.2, inplace=True)

        def forward(self, x):
            x1 = self.lrelu(self.conv1(x))
            x2 = self.lrelu(self.conv2(torch.cat((x, x1), 1)))
            x3 = self.lrelu(self.conv3(torch.cat((x, x1, x2), 1)))
            x4 = self.lrelu(self.conv4(torch.cat((x, x1, x2, x3), 1)))
            x5 = self.conv5(torch.cat((x, x1, x2, x3, x4), 1))
            return x5 * 0.2 + x

    class RRDB(nn.Module):
        def __init__(self, num_feat, num_grow_ch=32):
            super(RRDB, self).__init__()
            self.rdb1 = ResidualDenseBlock(num_feat, num_grow_ch)
            self.rdb2 = ResidualDenseBlock(num_feat, num_grow_ch)
            self.rdb3 = ResidualDenseBlock(num_feat, num_grow_ch)

        def forward(self, x):
            out = self.rdb1(x)
            out = self.rdb2(out)
            out = self.rdb3(out)
            return out * 0.2 + x

    class RRDBNet(nn.Module):
        def __init__(self, num_in_ch=3, num_out_ch=3, num_feat=64, num_block=23, num_grow_ch=32, scale=4):
            super(RRDBNet, self).__init__()
            self.scale = scale
            self.conv_first = nn.Conv2d(num_in_ch, num_feat, 3, 1, 1)
            self.body = nn.Sequential(*[RRDB(num_feat, num_grow_ch) for _ in range(num_block)])
            self.conv_body = nn.Conv2d(num_feat, num_feat, 3, 1, 1)
            self.conv_up1 = nn.Conv2d(num_feat, num_feat, 3, 1, 1)
            self.conv_up2 = nn.Conv2d(num_feat, num_feat, 3, 1, 1)
            self.conv_hr = nn.Conv2d(num_feat, num_feat, 3, 1, 1)
            self.conv_last = nn.Conv2d(num_feat, num_out_ch, 3, 1, 1)
            self.lrelu = nn.LeakyReLU(negative_slope=0.2, inplace=True)

        def forward(self, x):
            feat = self.conv_first(x)
            body_feat = self.conv_body(self.body(feat))
            feat = feat + body_feat
            feat = self.lrelu(self.conv_up1(F.interpolate(feat, scale_factor=2, mode='nearest')))
            feat = self.lrelu(self.conv_up2(F.interpolate(feat, scale_factor=2, mode='nearest')))
            out = self.conv_last(self.lrelu(self.conv_hr(feat)))
            return out

    return RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64, num_block=num_block, num_grow_ch=32, scale=4)

# BasicSRのインポート試行
try:
    from basicsr.archs.rrdbnet_arch import RRDBNet
    from basicsr.utils.download_util import load_file_from_url
except ImportError:
    # BasicSRが利用できない場合の代替実装
    def load_file_from_url(url, model_dir, progress=True, file_name=None):
        os.makedirs(model_dir, exist_ok=True)
        if file_name is None:
            file_name = url.split('/')[-1]
        file_path = os.path.join(model_dir, file_name)
        if os.path.exists(file_path):
            return file_path
        response = requests.get(url, stream=True)
        with open(file_path, 'wb') as f:
            for chunk in response.iter_content(chunk_size=8192):
                if chunk:
                    f.write(chunk)
        return file_path

# Real-ESRGANエンハンサークラス
class RealESRGANer:
    def __init__(self, scale, model_path, model, tile=0, tile_pad=10, pre_pad=0, half=True, device=None):
        self.scale = scale
        self.tile_size = tile
        self.tile_pad = tile_pad
        self.pre_pad = pre_pad
        self.half = half
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') if device is None else device

        loadnet = torch.load(model_path, map_location=torch.device('cpu'))
        if 'params_ema' in loadnet:
            keyname = 'params_ema'
        else:
            keyname = 'params'
        model.load_state_dict(loadnet[keyname], strict=True)
        model.eval()
        self.model = model.to(self.device)
        if self.half and self.device.type == 'cuda':
            self.model = self.model.half()

    def enhance(self, img, outscale=None):
        if outscale is None:
            outscale = self.scale

        img = img.astype(np.float32)
        if np.max(img) > 256:
            max_range = 65535
            img = img / max_range
        else:
            max_range = 255
            img = img / max_range

        if len(img.shape) == 2:
            img = np.expand_dims(img, axis=2)
        if img.shape[2] == 4:
            img = img[:, :, :3]

        h, w = img.shape[0:2]
        img = torch.from_numpy(np.transpose(img, (2, 0, 1))).float()
        img = img.unsqueeze(0).to(self.device)
        if self.half and self.device.type == 'cuda':
            img = img.half()

        # 自動GPU/CPUフォールバック機能(改善版)
        try:
            if torch.cuda.is_available() and self.device.type == 'cuda':
                max_memory = torch.cuda.max_memory_allocated()
                if max_memory > 0:
                    memory_used = torch.cuda.memory_allocated() / max_memory
                else:
                    memory_used = torch.cuda.memory_allocated() / torch.cuda.get_device_properties(0).total_memory

                if memory_used > 0.9:
                    img = img.cpu()
                    self.model = self.model.cpu()
                    self.device = torch.device('cpu')

            with torch.no_grad():
                output = self.model(img)
        except RuntimeError as e:
            if 'out of memory' in str(e).lower():
                print('GPU メモリ不足、CPUに切り替えます...')
                torch.cuda.empty_cache()
                img = img.cpu()
                self.model = self.model.cpu()
                self.device = torch.device('cpu')
                with torch.no_grad():
                    output = self.model(img)
            else:
                raise e

        output = output.data.squeeze().float().cpu().clamp_(0, 1).numpy()
        output = np.transpose(output, (1, 2, 0))

        if outscale != self.scale:
            output = cv2.resize(output, (int(w * outscale), int(h * outscale)), interpolation=cv2.INTER_LANCZOS4)

        output = (output * max_range).round().astype(np.uint8)
        return output, None

# モデル自動ダウンロード
weights_dir = Path(WEIGHTS_DIR)
weights_dir.mkdir(exist_ok=True)
model_path = weights_dir / f'{MODEL_NAME}.pth'
if not model_path.exists():
    print(f'モデル {MODEL_NAME} をダウンロード中...')
    url = MODEL_URLS[MODEL_NAME]
    load_file_from_url(url, model_dir=str(weights_dir), progress=True, file_name=f'{MODEL_NAME}.pth')

# モデル初期化
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
if 'anime' in MODEL_NAME:
    try:
        model = RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64, num_block=6, num_grow_ch=32, scale=4)
    except NameError:
        model = create_rrdbnet_model(6)
else:
    try:
        model = RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64, num_block=23, num_grow_ch=32, scale=4)
    except NameError:
        model = create_rrdbnet_model(23)

upsampler = RealESRGANer(
    scale=4,
    model_path=str(model_path),
    model=model,
    tile=TILE_SIZE,
    tile_pad=10,
    pre_pad=0,
    half=USE_HALF,
    device=device
)

# 処理結果記録用
processing_results = []
last_print_time = time.time()

def video_processing(frame, frame_count, fps):
    global last_print_time

    # フレーム改善
    frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    enhanced_frame, _ = upsampler.enhance(np.array(frame_rgb), outscale=SCALE_FACTOR)
    enhanced_frame_bgr = cv2.cvtColor(enhanced_frame, cv2.COLOR_RGB2BGR)

    # 処理情報を画面に表示
    info_text = f'Frame: {frame_count} | FPS: {fps:.1f} | Scale: {SCALE_FACTOR}x'
    cv2.putText(enhanced_frame_bgr, info_text, TEXT_POSITION, cv2.FONT_HERSHEY_SIMPLEX,
                FONT_SIZE, FONT_COLOR, FONT_THICKNESS)

    # 1秒間隔でprint出力
    current_time = time.time()
    if current_time - last_print_time >= 1.0:
        result_text = f'処理中 - フレーム: {frame_count}, 解像度: {frame.shape[1]}x{frame.shape[0]} → {enhanced_frame_bgr.shape[1]}x{enhanced_frame_bgr.shape[0]}'
        print(result_text)
        processing_results.append(result_text)
        last_print_time = current_time

    return enhanced_frame_bgr

# 入力選択
print('0: 動画ファイル')
print('1: カメラ')
print('2: サンプル動画')

choice = input('選択: ')
temp_file = None

if choice == '0':
    root = tk.Tk()
    root.withdraw()
    path = filedialog.askopenfilename()
    if not path:
        exit()
    cap = cv2.VideoCapture(path)
elif choice == '1':
    cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
    cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
elif choice == '2':
    # サンプル動画ダウンロード・処理
    url = 'https://github.com/opencv/opencv/raw/master/samples/data/vtest.avi'
    try:
        urllib.request.urlretrieve(url, SAMPLE_VIDEO_FILENAME)
        temp_file = SAMPLE_VIDEO_FILENAME
        cap = cv2.VideoCapture(SAMPLE_VIDEO_FILENAME)
    except Exception as e:
        print(f'動画のダウンロードに失敗しました: {url}')
        print(f'エラー: {e}')
        exit()
else:
    print('無効な選択です')
    exit()

# 動画情報取得
fps = int(cap.get(cv2.CAP_PROP_FPS))
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
original_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
original_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

# 音声抽出(動画ファイルの場合のみ)
has_audio = False
if choice != '1' and FFMPEG_AVAILABLE:
    if choice == '0':
        audio_source = path
    else:
        audio_source = SAMPLE_VIDEO_FILENAME
    audio_cmd = ['ffmpeg', '-i', audio_source, '-vn', '-acodec', 'copy', TEMP_AUDIO_FILE, '-y']
    has_audio = subprocess.run(audio_cmd, capture_output=True).returncode == 0

# 出力動画設定(動画ファイルの場合のみ)
if choice != '1':
    output_width = original_width * SCALE_FACTOR
    output_height = original_height * SCALE_FACTOR
    fourcc = cv2.VideoWriter_fourcc(*'mp4v')
    temp_video = TEMP_VIDEO_FILE if has_audio else OUTPUT_VIDEO_FILE
    out = cv2.VideoWriter(temp_video, fourcc, fps, (output_width, output_height))

print(f'\n動画処理開始')
print(f'解像度: {original_width}x{original_height} → {original_width * SCALE_FACTOR}x{original_height * SCALE_FACTOR}')
print(f'自動最適化設定: タイルサイズ={TILE_SIZE}, 半精度={USE_HALF}')
print(f'デバイス: {device}\n')

# メイン処理
frame_count = 0
try:
    while True:
        cap.grab()
        ret, frame = cap.retrieve()
        if not ret:
            break

        frame_count += 1
        processed_frame = video_processing(frame, frame_count, fps)

        # リアルタイム表示
        cv2.imshow('Real-ESRGAN Enhanced Video', processed_frame)

        # 動画ファイルへの書き込み(カメラ以外)
        if choice != '1':
            out.write(processed_frame)

        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
finally:
    cap.release()
    cv2.destroyAllWindows()
    if choice != '1':
        out.release()
    if temp_file:
        os.remove(temp_file)

# 音声結合(動画ファイルの場合のみ)
if choice != '1' and has_audio and FFMPEG_AVAILABLE:
    merge_cmd = ['ffmpeg', '-i', temp_video, '-i', TEMP_AUDIO_FILE, '-c:v', 'copy', '-c:a', 'aac', OUTPUT_VIDEO_FILE, '-y']
    if subprocess.run(merge_cmd, capture_output=True).returncode == 0:
        os.remove(temp_video)
        os.remove(TEMP_AUDIO_FILE)
        print('音声結合完了')
    else:
        if temp_video != OUTPUT_VIDEO_FILE:
            os.rename(temp_video, OUTPUT_VIDEO_FILE)
        if os.path.exists(TEMP_AUDIO_FILE):
            os.remove(TEMP_AUDIO_FILE)
        print('音声結合失敗、動画のみ保存')

# 品質評価(動画ファイルの場合のみ)
if choice != '1' and os.path.exists(OUTPUT_VIDEO_FILE):
    print('\n品質評価中...')
    if choice == '0':
        original_path = path
    else:
        original_path = SAMPLE_VIDEO_FILENAME

    cap_original = cv2.VideoCapture(original_path)
    cap_enhanced = cv2.VideoCapture(OUTPUT_VIDEO_FILE)

    sample_frames = min(10, total_frames)
    frame_indices = np.linspace(0, total_frames-1, sample_frames, dtype=int)

    psnr_values = []
    ssim_values = []

    for frame_idx in frame_indices:
        cap_original.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
        cap_enhanced.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)

        ret1, frame_orig = cap_original.read()
        ret2, frame_enh = cap_enhanced.read()

        if ret1 and ret2:
            original_resized = cv2.resize(frame_orig, (frame_enh.shape[1], frame_enh.shape[0]))
            psnr_val = psnr(original_resized, frame_enh, data_range=255)
            ssim_val = ssim(original_resized, frame_enh, channel_axis=2)
            psnr_values.append(psnr_val)
            ssim_values.append(ssim_val)

    cap_original.release()
    cap_enhanced.release()

    if psnr_values:
        avg_psnr = np.mean(psnr_values)
        avg_ssim = np.mean(ssim_values)

        quality_result = f'\n品質評価結果:\n平均PSNR: {avg_psnr:.2f} dB\n平均SSIM: {avg_ssim:.4f}'
        print(quality_result)
        processing_results.append(quality_result)

# 結果をファイルに保存
with open(RESULT_FILE, 'w', encoding='utf-8') as f:
    f.write('=== Real-ESRGAN動画品質改善処理結果 ===\n')
    f.write(f'モデル: {MODEL_NAME}\n')
    f.write(f'スケール: {SCALE_FACTOR}x\n')
    f.write(f'デバイス: {device}\n\n')
    for result in processing_results:
        f.write(result + '\n')

print(f'\n{RESULT_FILE}に保存しました')
print('処理完了')