ノイズ除去 (バイラテラルフィルタ)、彩度調整、ガンマ補正、コントラスト調整 (CLAHE)によるリアルタイム動画補正(ソースコードと説明と利用ガイド)

概要

このページのリアルタイム動画補正プログラムは、動画ファイルやウェブカメラ映像に対し、GUIスライダーを用いて、ノイズ除去 (バイラテラルフィルタ)、彩度調整、ガンマ補正、コントラスト調整 (CLAHE) を対話的に適用できるものである。調整したパラメータはプリセットとしてファイルに保存・読み込みが可能であり、画像処理の学習教材としての活用も意図している。

#Python #OpenCV #画像処理 #画像補正

目次

第1章 プログラム利用ガイド

1. このツールの利用シーン

このツールは、動画ファイルやウェブカメラの映像をリアルタイムで美しく補正するソフトウェアである。暗い映像を明るくしたり、色あせた映像を鮮やかにしたりといった調整を、結果を確認しながら直感的に行うことができる。ウェブ会議の映像改善や動画コンテンツの基本的な色補正などに活用される。

2. 主な機能

3. 基本的な使い方

  1. 起動と入力の選択

    プログラムを起動すると、コマンドプロンプト(黒い画面)で入力方法を選択する画面が表示される。キーボードで0(動画ファイル)、1(ウェブカメラ)、2(サンプル動画)のいずれかを入力し、Enterキーを押す。

  2. 画質の調整

    映像のプレビュー画面と「パラメータ調整」ウィンドウが表示される。「パラメータ調整」ウィンドウのスライダーを操作し、好みの画質になるよう調整する。変更はリアルタイムでプレビュー画面に反映される。

  3. 終了方法

    映像が表示されているウィンドウを選択した状態でキーボードのqキーを押すか、「パラメータ調整」ウィンドウの「終了」ボタンをクリックする。

4. 便利な機能

第2章 Python開発環境,ライブラリ類

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

第3章 Python 3.12 のインストール

Pythonのインストールを行い、Pythonのプログラムを実行する環境を整える。扱う環境は、Windows搭載パソコンである。金子研究室では、Python 3.12.10を推奨する。

[Windows での Python 3.12 のインストール手順を見るには、ここをクリック]

Windows での Python 3.12 のインストール

以下のいずれかの方法でPython 3.12をインストールする。Pythonがインストール済みの場合、この手順は不要である。

方法 1:winget によるインストール

インストールコマンドの実行方法

管理者権限コマンドプロンプトを起動する(手順:Windowsキーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。そして、コマンド全体をコマンドプロンプトにコピー&ペーストする。

--scope machine を指定することで、システム全体(全ユーザー向け)にインストールされる。このオプションの実行には管理者権限が必要である。インストール完了後、コマンドプロンプトを再起動するとPATHが反映される。

REM Python 3.12 をシステム領域にインストール
winget install --id Python.Python.3.12 -e --scope machine --silent --accept-source-agreements --accept-package-agreements --override "/quiet InstallAllUsers=1 PrependPath=1 Include_test=0 Include_pip=1 Include_launcher=1 InstallLauncherAllUsers=1 TargetDir=\"C:\Program Files\Python312\""

REM Python と Scripts を PATH 先頭に追加
powershell -NoProfile -Command "$p='C:\Program Files\Python312'; $s=\"$p\Scripts\"; $c=[Environment]::GetEnvironmentVariable('Path','Machine'); if((Test-Path $p) -and (';'+$c+';' -notlike \"*;$p;*\") -and (';'+$c+';' -notlike \"*;$s;*\")){[Environment]::SetEnvironmentVariable('Path',\"$p;$s;$c\",'Machine')}"

方法 2:インストーラーによるインストール

  1. Python公式サイト(https://www.python.org/downloads/)にアクセスし、「Download Python 3.x.x」ボタンからWindows用インストーラーをダウンロードする。
  2. ダウンロードしたインストーラーを実行する。
  3. 初期画面の下部に表示される「Add python.exe to PATH」にチェックを入れてから「Customize installation」を選択する。このチェックを入れ忘れると、コマンドプロンプトから python コマンドを実行できない。
  4. 「Install Python 3.xx for all users」にチェックを入れ、「Install」をクリックする。

インストールの確認

コマンドプロンプトで以下を実行する。

python --version

バージョン番号(例:Python 3.12.x)が表示されればインストール成功である。「'python' は、内部コマンドまたは外部コマンドとして認識されていません。」と表示される場合は、インストールが正常に完了していない。

第4章 Python の開発環境 Visual Studio Code のインストールと Python 用の設定

Python の開発環境Visual Studio Code(プログラムを編集するソフトウェア。以下、VS Code)を整える。

[Windows での Visual Studio Code のインストールと Python 用の設定手順を見るには、ここをクリック]

Windows での Visual Studio Code のインストールと Python 用の設定手順

1. VS Code と拡張機能のインストール

以下のコマンドにより,既存の VS Code を削除し,全ユーザー共有の設定で再インストールしたうえで,拡張機能(VS Code に機能を追加するソフトウェア)をまとめて導入する.

インストールコマンドの実行方法

管理者権限コマンドプロンプトを起動する(手順:Windows キーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。そして,コマンド全体をコマンドプロンプトにコピー&ペーストする。

インストールコマンド


REM ============================================================
REM Microsoft Visual Studio Code
REM ============================================================
winget uninstall -e --id Microsoft.VisualStudioCode --silent --disable-interactivity --accept-source-agreements
rmdir /s /q C:\ProgramData\vscode-extensions 2>nul
rmdir /s /q "%APPDATA%\Code" 2>nul
rmdir /s /q "%USERPROFILE%\.vscode" 2>nul
rmdir /s /q "%LOCALAPPDATA%\Microsoft\vscode-update" 2>nul

REM VS Code をシステム領域に新規インストール
winget install --scope machine --id Microsoft.VisualStudioCode -e --silent --accept-source-agreements --accept-package-agreements

REM 全ユーザー共有の拡張機能フォルダ
mkdir C:\ProgramData\vscode-extensions 2>nul
icacls "C:\ProgramData\vscode-extensions" /grant "Everyone:(OI)(CI)M" /T

REM スタートメニューのショートカットを --extensions-dir 付きで再作成
rmdir /s /q "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code" 2>nul
del "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk" 2>nul
powershell -NoProfile -Command "$s=New-Object -ComObject WScript.Shell; $lnk=$s.CreateShortcut('C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk'); $lnk.TargetPath='C:\Program Files\Microsoft VS Code\Code.exe'; $lnk.Arguments='--extensions-dir \"C:\ProgramData\vscode-extensions\"'; $lnk.Save()"
REM ショートカットの検証
powershell -NoProfile -Command "$s=New-Object -ComObject WScript.Shell; $lnk=$s.CreateShortcut('C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio Code.lnk'); Write-Host 'TargetPath:' $lnk.TargetPath; Write-Host 'Arguments:' $lnk.Arguments"

REM ファイル / フォルダ右クリックの「Code で開く」を登録
reg add "HKLM\SOFTWARE\Classes\*\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%1\"" /f
reg add "HKLM\SOFTWARE\Classes\Directory\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%1\"" /f
reg add "HKLM\SOFTWARE\Classes\Directory\Background\shell\VSCode\command" /ve /d "\"C:\Program Files\Microsoft VS Code\Code.exe\" --extensions-dir \"C:\ProgramData\vscode-extensions\" \"%V\"" /f

REM --extensions-dir 付きで起動する code.cmd ラッパを作成
REM (%* を echo で書くと対話的 cmd で失われるため、PowerShell で [char]37+'*' を書き出す)
powershell -NoProfile -Command "$pct=[char]37; $q=[char]34; $c='@echo off'+[char]13+[char]10+$q+'C:\Program Files\Microsoft VS Code\bin\code.cmd'+$q+' --extensions-dir '+$q+'C:\ProgramData\vscode-extensions'+$q+' '+$pct+'*'+[char]13+[char]10; [IO.File]::WriteAllText('C:\ProgramData\vscode-extensions\vscode.cmd',$c,[Text.Encoding]::ASCII)"

REM 拡張機能のインストール
set "CODE=C:\Program Files\Microsoft VS Code\bin\code.cmd"
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --uninstall-extension GitHub.copilot
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --uninstall-extension GitHub.copilot-chat
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.python
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.vscode-pylance
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension ms-python.debugpy
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension MS-CEINTL.vscode-language-pack-ja
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension saoudrizwan.claude-dev
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension rust-lang.rust-analyzer
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension tamasfe.even-better-toml
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension anthropic.claude-code
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --install-extension almenon.arepl
"%CODE%" --extensions-dir "C:\ProgramData\vscode-extensions" --list-extensions --show-versions
echo === セットアップ完了 ===

2. Python インタプリタの選択

同一マシンに複数の Python がインストールされている場合,VS Code で使用する Python 本体(インタプリタ:Python プログラムを解釈・実行するソフトウェア)を選択する必要がある.

  1. コマンドパレット(コマンド名で機能を呼び出す VS Code の入力欄)を開く(Ctrl+Shift+P
  2. Python: Select Interpreter と入力する
  3. 表示される一覧から,使用する Python(例:C:\Program Files\Python312\python.exe)を選択する.

必要なライブラリをシステム領域にインストール

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

pip install --no-user opencv-python numpy

第5章 ノイズ除去 (バイラテラルフィルタ)、彩度調整、ガンマ補正、コントラスト調整 (CLAHE)によるリアルタイム動画補正プログラム

概要

本プログラムは、動画ファイルまたはカメラからの映像に対し、複数の画像処理技術を適用してリアルタイムに画質を補正するツールである。利用者はGUIを通じて各種パラメータを対話的に調整し、その結果を即座にプレビューできる。また、調整したパラメータ設定の保存・読み込みや、補正後の映像を動画ファイルとして出力する機能も備える。

主要技術

本プログラムは、画質補正のために以下の主要な画像処理アルゴリズムを組み合わせている。

技術的特徴

本プログラムは、「ノイズ除去(バイラテラルフィルタ)」、「彩度調整」、「輝度調整(ガンマ補正)」、「コントラスト調整(CLAHE)」の順に行われる。特にノイズ除去を初期段階で行うことで、以降の処理におけるノイズ増幅を抑制している。また、CLAHEを適用する際には、輝度チャンネル(Lチャンネル)にのみ処理を施すことで、元の色彩情報を損なうことなくコントラストを改善している。

実装の特色

本プログラムは、OpenCVによる画像処理とtkinterによるGUI操作を単一のループ内で同期させている。これにより、パラメータ変更をリアルタイムで映像に反映させることが可能となっている。また、調整したパラメータ群は、JSON形式の外部ファイルとして保存・読み込みが可能であり、特定の設定をプリセットとして再現できる。さらに、GUIで設定したパラメータを用いて動画全体を非対話的に処理し、新しい動画ファイルとして書き出すバッチ処理機能も実装されている。

参考文献

ソースコード

# プログラム名: ノイズ除去 (バイラテラルフィルタ)、彩度調整、ガンマ補正、コントラスト調整 (CLAHE)によるリアルタイム動画補正プログラム
# プログラム名: リアルタイム動画補正プログラム
# 特徴技術名: CLAHE (Contrast Limited Adaptive Histogram Equalization)
# 出典: Bradski, G., & Kaehler, A. (2008). Learning OpenCV: Computer Vision with the OpenCV Library. O'Reilly Media, Inc.
# 特徴機能: 画像を小さな領域(タイル)に分割し、各領域でヒストグラム均等化を行います。その際、ノイズが増幅されるのを防ぐためにコントラストに上限(クリップリミット)を設定する機能です。これにより、画像全体の照明のむらを補正しつつ、局所的なディテールを鮮明にすることが可能です。
# 学習済みモデル: なし
# 特徴技術および学習済モデルの利用制限: 本プログラムが利用する技術(OpenCV)は、Apache License 2.0の下で提供されており、商用利用も可能ですが、一部特許で保護されたアルゴリズムが含まれる可能性があります。詳細は公式のライセンス情報を参照し、必ず利用者自身で利用制限を確認してください。
# 方式設計:
#     - 関連利用技術:
#         - OpenCV: 動画の入出力、画像処理(バイラテラルフィルタ、彩度調整、ガンマ補正、色空間変換、CLAHE)に使用します。
#         - tkinter: 各種補正パラメータをリアルタイムで調整するためのGUI(スライダー)を提供します。
#     - 入力と出力:
#         入力: 動画(ユーザは「0:動画ファイル,1:カメラ,2:サンプル動画」のメニューで選択.0:動画ファイルの場合はtkinterでファイル選択.1の場合はOpenCVでカメラが開く.2の場合はhttps://raw.githubusercontent.com/opencv/opencv/master/samples/data/vtest.aviを使用)
#         出力: 処理結果をOpenCVウィンドウでリアルタイムに表示します(左側:元画像、右側:処理後)。また、プログラム終了時に各フレームで適用されたパラメータ設定を`result.txt`に保存します。
#     - 処理手順:
#         1. tkinterで作成したスライダーから各種パラメータを取得します。
#         2. 入力動画のフレームに対し、ノイズ除去のためバイラテラルフィルタを適用します。
#         3. 次に、HSV色空間に変換し、彩度を調整します。
#         4. その後、ガンマ補正を適用します。
#         5. 最後に、LAB色空間に変換し、輝度チャンネルにCLAHEを適用してコントラストを調整します。
#         6. 元のフレームと処理後のフレームを水平に連結し、一つのウィンドウに表示します。
#     - 前処理、後処理: 本プログラムでは、特徴技術の効果を発揮させるための特別な前処理および後処理は実装しません。
#     - 追加処理: ユーザがGUIを通じてリアルタイムにパラメータを変更できるようにするため、OpenCVのメインループ内でtkinterのイベントループを更新する処理を追加します。
#     - 調整を必要とする設定値:
#         - bilateral_d: バイラテラルフィルタの近傍領域の直径。0で無効化。
#         - sigma_color: バイラテラルフィルタの色空間におけるシグマ値。
#         - sigma_space: バイラテラルフィルタの座標空間におけるシグマ値。
#         - saturation: 彩度の倍率。1.0で無補正。
#         - gamma: ガンマ補正の強度。1.0で無補正。
#         - clip_limit: CLAHEにおけるコントラストの上限値。
#         - tile_grid_size: CLAHEが画像を分割する際のタイル(格子)のサイズ。
# 将来方策: 現在は手動で調整しているパラメータについて、画像の特性(ヒストグラムなど)を分析し、最適な値を自動で提案する機能を将来的に追加することが考えられます。
# 前準備: pip install --no-user opencv-python numpy

import cv2
import numpy as np
import tkinter as tk
from tkinter import filedialog, messagebox
import urllib.request
import os
import time
from datetime import datetime
import json

# --- グローバル変数 ---
frame_count = 0
results_log = []
current_input_path = None
exit_requested = False
# Tkinter関連の変数
root_tk = None
gamma_var = None
clip_limit_var = None
tile_size_var = None
bilateral_d_var = None
sigma_color_var = None
sigma_space_var = None
saturation_var = None

# --- ヘルパー関数 ---
def create_slider(parent, text, from_, to, resolution, variable):
    tk.Label(parent, text=text).pack()
    slider = tk.Scale(parent, from_=from_, to=to, resolution=resolution, orient=tk.HORIZONTAL, variable=variable)
    slider.pack(fill=tk.X, padx=10)
    return slider

def apply_image_processing(frame, params):
    processed_frame = frame.copy()
    if params['d'] > 0:
        processed_frame = cv2.bilateralFilter(processed_frame, params['d'], params['sigma_color'], params['sigma_space'])
    if params['saturation_scale'] != 1.0:
        hsv = cv2.cvtColor(processed_frame, cv2.COLOR_BGR2HSV)
        h, s, v = cv2.split(hsv)
        s = np.clip(s.astype(np.float32) * params['saturation_scale'], 0.0, 255.0).astype(np.uint8)
        processed_frame = cv2.cvtColor(cv2.merge((h, s, v)), cv2.COLOR_HSV2BGR)
    if params['gamma'] != 1.0:
        look_up_table = np.array([((i / 255.0) ** (1.0 / params['gamma'])) * 255 for i in np.arange(0, 256)]).astype("uint8")
        processed_frame = cv2.LUT(processed_frame, look_up_table)
    lab_frame = cv2.cvtColor(processed_frame, cv2.COLOR_BGR2LAB)
    l_channel, a_channel, b_channel = cv2.split(lab_frame)
    clahe = cv2.createCLAHE(clipLimit=params['clip_limit'], tileGridSize=(params['tile_size'], params['tile_size']))
    clahe_l_channel = clahe.apply(l_channel)
    processed_frame = cv2.cvtColor(cv2.merge([clahe_l_channel, a_channel, b_channel]), cv2.COLOR_LAB2BGR)
    return processed_frame

def video_frame_processing(frame):
    global frame_count
    current_time = time.time()
    frame_count += 1
    params = {
        'd': bilateral_d_var.get(), 'sigma_color': sigma_color_var.get(), 'sigma_space': sigma_space_var.get(),
        'saturation_scale': saturation_var.get(), 'gamma': gamma_var.get(),
        'clip_limit': clip_limit_var.get(), 'tile_size': tile_size_var.get()
    }
    processed_frame = apply_image_processing(frame, params)
    # 表示順を「左:元画像, 右:処理後」に変更
    combined_frame = np.hstack((frame, processed_frame))
    result = f"Bilateral(d:{params['d']},cσ:{params['sigma_color']},sσ:{params['sigma_space']}), Sat:{params['saturation_scale']:.2f}, Gamma:{params['gamma']:.2f}, CLAHE(clip:{params['clip_limit']:.1f},tile:{params['tile_size']})"
    return combined_frame, result, current_time

def request_exit():
    global exit_requested
    exit_requested = True

def save_preset():
    preset_data = {
        "bilateral_d": bilateral_d_var.get(),
        "sigma_color": sigma_color_var.get(),
        "sigma_space": sigma_space_var.get(),
        "saturation": saturation_var.get(),
        "gamma": gamma_var.get(),
        "clip_limit": clip_limit_var.get(),
        "tile_grid_size": tile_size_var.get()
    }
    try:
        with open("preset.json", "w", encoding="utf-8") as f:
            json.dump(preset_data, f, indent=4)
        messagebox.showinfo("成功", "設定を preset.json に保存しました。")
    except Exception as e:
        messagebox.showerror("エラー", f"設定の保存中にエラーが発生しました:\n{e}")

def load_preset():
    try:
        if not os.path.exists("preset.json"):
            messagebox.showwarning("警告", "preset.json が見つかりません。")
            return
        with open("preset.json", "r", encoding="utf-8") as f:
            preset_data = json.load(f)

        bilateral_d_var.set(preset_data.get("bilateral_d", 0))
        sigma_color_var.set(preset_data.get("sigma_color", 75))
        sigma_space_var.set(preset_data.get("sigma_space", 75))
        saturation_var.set(preset_data.get("saturation", 1.0))
        gamma_var.set(preset_data.get("gamma", 1.0))
        clip_limit_var.set(preset_data.get("clip_limit", 2.0))
        tile_size_var.set(preset_data.get("tile_grid_size", 8))

        messagebox.showinfo("成功", "設定を読み込みました。")
    except json.JSONDecodeError:
        messagebox.showerror("エラー", "preset.json の形式が正しくありません。")
    except Exception as e:
        messagebox.showerror("エラー", f"設定の読み込み中にエラーが発生しました:\n{e}")

def reset_parameters():
    """スライダーをすべて初期値に戻す"""
    bilateral_d_var.set(0)
    sigma_color_var.set(75)
    sigma_space_var.set(75)
    saturation_var.set(1.0)
    gamma_var.set(1.0)
    clip_limit_var.set(2.0)
    tile_size_var.set(8)

def save_video_file():
    global current_input_path
    if not current_input_path:
        messagebox.showerror("エラー", "保存対象の動画ファイルがありません。")
        return

    output_path = filedialog.asksaveasfilename(defaultextension=".mp4", filetypes=[("MP4 files", "*.mp4"), ("All files", "*.*")])
    if not output_path:
        return

    params = {
        'd': bilateral_d_var.get(), 'sigma_color': sigma_color_var.get(), 'sigma_space': sigma_space_var.get(),
        'saturation_scale': saturation_var.get(), 'gamma': gamma_var.get(),
        'clip_limit': clip_limit_var.get(), 'tile_size': tile_size_var.get()
    }

    cap_save = cv2.VideoCapture(current_input_path)
    if not cap_save.isOpened():
        messagebox.showerror("エラー", "入力動画ファイルを開けませんでした。")
        return

    fps = cap_save.get(cv2.CAP_PROP_FPS)
    width = int(cap_save.get(cv2.CAP_PROP_FRAME_WIDTH))
    height = int(cap_save.get(cv2.CAP_PROP_FRAME_HEIGHT))
    total_frames = int(cap_save.get(cv2.CAP_PROP_FRAME_COUNT))

    writer = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'XVID'), fps, (width, height))

    print("\n=== 動画ファイル保存開始 ===")
    print(f"入力: {os.path.basename(current_input_path)}")
    print(f"出力: {os.path.basename(output_path)}")

    for i in range(total_frames):
        ret, frame = cap_save.read()
        if not ret:
            break
        processed_frame = apply_image_processing(frame, params)
        writer.write(processed_frame)
        progress = (i + 1) / total_frames * 100
        print(f"\r処理中: {i + 1}/{total_frames} ({progress:.2f}%)", end="")

    print("\n=== 動画ファイル保存完了 ===")
    cap_save.release()
    writer.release()
    messagebox.showinfo("完了", "動画の保存が完了しました。")

# --- メイン処理 ---
print("リアルタイム動画補正プログラム")
print("補正パラメータをGUIで調整できます。")
print("\n入力ソースを選択してください:")
print("0: 動画ファイル")
print("1: カメラ")
print("2: サンプル動画")

choice = input("選択: ")

cap = None
if choice == '0':
    temp_root = tk.Tk()
    temp_root.withdraw()
    path = filedialog.askopenfilename()
    temp_root.destroy()
    if not path:
        exit()
    current_input_path = path
    cap = cv2.VideoCapture(current_input_path)
elif choice == '1':
    cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
    if not cap.isOpened():
        cap = cv2.VideoCapture(0)
elif choice == '2':
    SAMPLE_URL = 'https://raw.githubusercontent.com/opencv/opencv/master/samples/data/vtest.avi'
    SAMPLE_FILE = 'vtest.avi'
    try:
        urllib.request.urlretrieve(SAMPLE_URL, SAMPLE_FILE)
        current_input_path = os.path.abspath(SAMPLE_FILE)
        cap = cv2.VideoCapture(current_input_path)
    except Exception as e:
        print(f"ダウンロードに失敗しました: {e}")
        exit()
else:
    print("無効な選択です。")
    exit()

if not cap.isOpened():
    print('動画ファイル・カメラを開けませんでした')
    exit()

root_tk = tk.Tk()
root_tk.title("パラメータ調整")
root_tk.geometry("300x630")

bilateral_d_var = tk.IntVar(value=0)
sigma_color_var = tk.IntVar(value=75)
sigma_space_var = tk.IntVar(value=75)
saturation_var = tk.DoubleVar(value=1.0)
gamma_var = tk.DoubleVar(value=1.0)
clip_limit_var = tk.DoubleVar(value=2.0)
tile_size_var = tk.IntVar(value=8)

create_slider(root_tk, "バイラテラルフィルタ径 d (0-15)", 0, 15, 1, bilateral_d_var)
create_slider(root_tk, "色空間シグマ σColor (0-200)", 0, 200, 1, sigma_color_var)
create_slider(root_tk, "座標空間シグマ σSpace (0-200)", 0, 200, 1, sigma_space_var)
create_slider(root_tk, "彩度 (0.0-3.0)", 0.0, 3.0, 0.1, saturation_var)
create_slider(root_tk, "ガンマ (0.1-5.0)", 0.1, 5.0, 0.1, gamma_var)
create_slider(root_tk, "CLAHE クリップリミット (1.0-40.0)", 1.0, 40.0, 1.0, clip_limit_var)
create_slider(root_tk, "CLAHE タイルサイズ (1-32)", 1, 32, 1, tile_size_var)

preset_frame = tk.Frame(root_tk)
preset_frame.pack(pady=5)
load_button = tk.Button(preset_frame, text="設定を読み込み", command=load_preset)
load_button.pack(side=tk.LEFT, padx=5)
save_preset_button = tk.Button(preset_frame, text="設定を保存", command=save_preset)
save_preset_button.pack(side=tk.LEFT, padx=5)
reset_button = tk.Button(preset_frame, text="リセット", command=reset_parameters)
reset_button.pack(side=tk.LEFT, padx=5)

save_button_state = tk.NORMAL if choice != '1' else tk.DISABLED
save_button = tk.Button(root_tk, text="動画をファイルに保存...", command=save_video_file, state=save_button_state)
save_button.pack(pady=5)

exit_button = tk.Button(root_tk, text="終了", command=request_exit)
exit_button.pack(pady=5)

print('\n=== 動画処理開始 ===')
print('操作方法:')
print('  q キー: プログラム終了')
print('  r キー: 動画の先頭に戻る (動画ファイルのみ)')
print('  終了ボタン: GUIからプログラムを終了')
try:
    # ウィンドウタイトルを「左:元画像, 右:処理後」に変更
    MAIN_FUNC_DESC = "画像補正 (左: 元画像, 右: 処理後)"
    while True:
        if exit_requested:
            break

        ret, frame = cap.read()
        if not ret:
            if choice != '1':
                cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
                frame_count = 0
                continue
            else:
                break

        processed_frame, result, current_time = video_frame_processing(frame)
        cv2.imshow(MAIN_FUNC_DESC, processed_frame)
        log_entry = f"{datetime.fromtimestamp(current_time).strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]} {result}" if choice == '1' else f"Frame: {frame_count}, {result}"
        print(log_entry)
        results_log.append(log_entry)
        root_tk.update()

        key = cv2.waitKey(1) & 0xFF
        if key == ord('q'):
            break
        elif key == ord('r') and choice != '1':
            cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
            frame_count = 0
            results_log.clear()
            print("\n--- 動画の先頭に戻りました ---")
finally:
    print('\n=== プログラム終了 ===')
    if cap:
        cap.release()
    cv2.destroyAllWindows()
    if root_tk:
        root_tk.destroy()
    if results_log:
        with open('result.txt', 'w', encoding='utf-8') as f:
            f.write('=== 結果 ===\n')
            f.write(f'処理フレーム数: {frame_count}\n\n')
            f.write('\n'.join(results_log))
        print(f'\n処理結果をresult.txtに保存しました')