低照度化と輝度補正が物体検出モデルの検出結果に与える影響の測定
【概要】
同一フレームに人工的な照度低下(ガンマ変換。画素値をべき乗して明るさを非線形に変える処理)と輝度補正の組合せを与え、物体検出結果の変化を測定する資料である。COCO(80種類の物体に注釈が付与された画像データセット)で学習済みのYOLO26nを照度3水準×補正5条件の計15条件に適用し、15条件の検出画像と測定値を5列×3行の格子に同時表示するプログラムの手順・コード・考察の視点をまとめる。
【目次】
1. 目的と前提
目的は、同一フレームに人工的な照度低下(ガンマ変換)と輝度補正の組合せを与え、物体検出結果の変化を測定することである。実装は、COCO学習済みYOLO26nを、照度3水準×補正5条件の計15条件に適用し、15条件の検出画像と測定値を5列×3行の格子に同時表示するPython/OpenCV/tkinterプログラムである。
15条件は1枚のフレームから生成され、5本の補正列は同一の劣化画像を共有する。したがって列間の差は補正手法のみに、行間の差は照度のみに帰属する。15条件は常に同時に表示され、切り替えによって隠れるものはない。
| 列 | 補正条件 | この列が担う問い |
|---|---|---|
| 第1列 | 補正なし | 照度低下そのものが検出結果に与える影響はどれだけか。全列共通の対照条件である。 |
| 第2列 | CLAHE(コントラスト制限付き適応ヒストグラム均等化) | 局所的なコントラスト強調で検出結果は回復するか。 |
| 第3列 | AGCWD(重み付け分布付き適応ガンマ補正) | 補正の強さを画像統計から自動決定する手法は、照度低下に追随できるか。 |
| 第4列 | AGCWD→CLAHE | 大域的トーン補正に局所コントラスト強調を重ねると、効果は加算的か。 |
| 第5列 | LIME型照明マップ推定(LIMEは低照度画像の補正手法の名称) | CLAHEに依存しない補正経路で、どこまで回復するか。 |
行は上から、劣化ガンマ指数1.00(基準条件、元フレームと一致)、中段の値、下段の値である。
夜間の監視カメラ、悪条件下の自動運転、低照度の検査画像など、実環境では照度が保証されない場合がある。学習済みモデルがどの照度で機能を失い、前処理でどこまで取り戻せるかを数値で把握することは、実用システムにおける安全余裕の設定根拠となる。
2. 出力データ
出力は、15マスの格子画像、コンソールへの測定値、CSVファイル(measurement_log.csv、1条件1行)である。各条件について次の量を測る。
- 検出数 n:確信度(モデルが出力する検出の確からしさ。0から1の値)がしきい値conf以上である検出の個数。
- 平均確信度:それらn個の確信度の算術平均。検出できた物体だけの平均であるため、検出数が減ると母数も減る。n=0のときは定義されず「-」と表示する。
- 参照一致数 m、参照再現率、参照適合率:左上のマス(劣化ガンマ指数1.00・補正なし)の検出集合を参照集合とし、クラスが一致しIoU(2つの矩形の共通部分の面積を和集合の面積で割った値)がしきい値以上の検出を1対1で対応づけた数がmである。参照再現率はm÷参照集合の要素数、参照適合率はm÷nである。参照集合はモデル自身の出力であり、人手による正解ラベルではない。これらは「基準条件の出力をどれだけ保っているか」を表す。検出数nだけでは、増えた検出が正しいのか新たに生じた誤検出なのかを区別できないため、この2つを併記する。
- 平均明度L*、明度標準偏差:推論に入力される画像のCIELAB色空間(明度L*と2つの色度a*、b*で色を表す表色系)における明度L*(0〜100に換算)の平均と標準偏差。標準偏差はコンソールとCSVのみに出力する。
3. 手法候補の一覧
照度の変化(入力側の要因)と輝度・コントラストの補正(前処理側の要因)が、物体検出モデル(測定器としての要因)の出力に与える影響を測る。したがって候補は、輝度・コントラストを操作する手法と、検出を担うモデルの両方から選定する。
| No. | 手法名 | 発表 | 分類 | 概要 |
|---|---|---|---|---|
| 1 | ヒストグラム均等化(HE) | 古典的手法 | 大域的な輝度変換 | 画像全体の輝度ヒストグラムを累積分布関数(ある値以下の画素が全体に占める割合を表す関数)によって写像し、階調の分布を一様に近づける。変換が画像全体で共通のため、画面の一部だけが暗い場合には、その部分の階調が広がらない。 |
| 2 | ガンマ変換 | 古典的手法 | 画素単位のべき乗変換 | 正規化した画素値をべき乗して輝度を非線形に変換する。指数の与え方によって明るくも暗くもできる。単調増加の変換であるため画素値の大小関係は保たれる。256要素のルックアップテーブル(入力値と出力値の対応表)で計算できる。 |
| 3 | CLAHE(コントラスト制限付き適応ヒストグラム均等化) | Zuiderveld, Graphics Gems IV, 1994 | 局所的なヒストグラム変換 | 画像を格子状のタイルに分割し、タイルごとにヒストグラム均等化を行う。クリップ制限値によってヒストグラムの高さを切り詰め、雑音の増幅量を制限する。タイル境界は双一次補間(周囲4点の値から線形に内挿する補間)で接続する。 |
| 4 | AGCWD(重み付け分布付き適応ガンマ補正) | Huang ら, IEEE TIP, 2013 | 大域的な統計依存トーン変換 | 入力画像の輝度確率密度関数(各輝度値の出現割合を表す分布)を指数αで重み付けし、その累積分布 cdfw(l) から階調ごとのガンマ指数 γ(l)=1−cdfw(l) を定める。補正の強さを人が与える必要がない。 |
| 5 | MSRCR(色復元付きマルチスケールRetinex) | Jobson ら, IEEE TIP, 1997 | Retinex理論に基づく分解 | Retinex理論(画像を照明成分と反射成分の積とみなす考え方)に基づき、広がりの異なる複数のガウシアン(ガウス関数の形をした平滑化フィルタ)で照明成分を推定し、反射成分を取り出す。色復元の項によって色の偏りを補正する。スケールと係数の設定を要する。 |
| 6 | LIME | Guo ら, IEEE TIP, 2017 | 照明マップの推定 | 各画素のRGB3成分の最大値から照明マップの初期値を求め、構造を保存する正則化(解が滑らかになるように制約を加える操作)によって平滑化したうえで、これを用いて除算し明るさを補正する。学習を行わず、最適化計算のみで動作する。 |
| 7 | ガイデッドフィルタ | He ら, IEEE TPAMI, 2013 | 構造保存平滑化 | 局所窓ごとに案内画像(平滑化の手がかりとして与える画像)との線形関係を最小二乗法で当てはめ、その係数を平均して出力する。エッジを保存したまま平滑化できる。No.6の照明マップの平滑化に利用できる。 |
| 8 | Zero-DCE(AIモデル:DCE-Net) | Guo ら, CVPR, 2020 | 深層学習(正解画像を用いない学習) | 畳み込みニューラルネットワークが画素ごとの輝度調整曲線の係数を推定する。明るい画像と暗い画像の対を用いず、露出や色の恒常性(照明が変わっても物体の色の見えが保たれる性質)に関する制約を損失関数として学習する。推定した曲線を反復適用して補正する。 |
| 9 | Retinexformer(AIモデル) | Cai ら, ICCV, 2023 | 深層学習(Transformer) | Retinex理論に基づく一段構成の枠組みに、照明を手がかりとする自己注意機構(入力要素どうしの関連の強さを計算して重み付けする仕組み)を組み合わせる。低照度画像復元のベンチマークで高い復元品質が報告されている。 |
| 10 | CoLIE(AIモデル) | Chobola ら, ECCV, 2024 | 深層学習(陰的神経表現) | 画像座標から輝度成分への写像を陰的神経表現(座標を入力として値を出力する小規模ネットワーク)で表し、画像1枚ごとに最適化する。学習済みデータセットへの依存が小さい一方、画像ごとに最適化計算を要する。 |
| 11 | YOLO26(AIモデル:yolo26nを含むn/s/m/l/xの5規模) | Ultralytics, 2026(arXiv:2606.03748) | NMSを用いない一段物体検出モデル | 既定の一対一ヘッド(1つの物体に検出枠を1つだけ対応づける出力部)により、NMS(非最大値抑制。重なり合う検出枠から代表を1つ残す後処理)を用いずに最大300個の検出を直接出力する。yolo26nのCOCO上のmAP50-95(物体検出の代表的な精度指標。IoUのしきい値を0.50から0.95まで変えて求めた検出精度の平均)は、既定の一対一ヘッドで40.1、NMSを併用する一対多ヘッドで40.9と報告されている。 |
| 12 | RT-DETR(AIモデル:rtdetr-l、rtdetr-x) | Zhao ら(Baidu), CVPR, 2024 | Transformer型の物体検出モデル | Transformer(自己注意機構を用いた深層学習の構造)に基づくDETR系(Transformerで物体検出を行うモデルの系列)の実時間検出器である。畳み込みによるバックボーン(画像から特徴を抽出する主要部)、ハイブリッドエンコーダ、IoUを考慮したクエリ選択(検出候補の絞り込み方)を用い、NMSを用いずに検出する。学習済み重みは規模lとxが配布されている。 |
4. コードで実装されている手法
本プログラムが実装しているのは、上表のNo.2(ガンマ変換)、No.3(CLAHE)、No.4(AGCWD)、No.6とNo.7の組合せ(LIME型照明マップ推定+ガイデッドフィルタ)、No.11(YOLO26、規模はn)である。役割は次のように分かれている。
- ガンマ変換:照度低下を人工的に作り出す操作である。画質の補正のためではなく、独立変数(実験者が変える量)としての暗さを与えるために用いる。
- CLAHE、AGCWD、LIME型照明マップ推定:暗くした画像に対する前処理であり、効果を測る対象そのものである。
- YOLO26n:検出結果を測る測定器である。1回の実験の中では重みもパラメータも変更しない。
4.1 他の候補手法と比べた特徴
- CLAHEはNo.1のヒストグラム均等化と異なり、タイル単位の局所処理である。このため、画面の一部だけが暗い場面でもその部分の階調を広げられる。また、クリップ制限値によって雑音の増幅量を制限できる点が、制限を持たない局所均等化との違いである。
- AGCWDはNo.2の固定ガンマ変換と異なり、指数を人が与えない。指数は入力画像の輝度分布から階調ごとに定まるため、入力の暗さに応じて補正の強さが変わる。
- LIME型照明マップ推定はNo.5のMSRCRと同じくRetinex理論に基づくが、複数スケールのガウシアンではなく、RGB最大値による初期推定と構造保存平滑化で照明マップを求める。設定すべきパラメータが少ない。
- No.8からNo.10の深層学習型は、文献において高い復元品質が報告されている。一方でこれらは学習済み重みを個別に取得する必要があり、1フレームあたり15条件の推論に加えると、GPUのない環境では画面の更新間隔が実用的でなくなる。採用した3手法はいずれも学習を伴わず、同じ入力に対して常に同じ出力を返し、追加の重みファイルを必要としない。
- No.12のRT-DETRとの相違点は、Ultralyticsが配布するRT-DETRの学習済み重みが規模lとxの2種であるのに対し、YOLO26はn/s/m/l/xの5規模を持ち、最小のyolo26nのパラメータ数が2.4Mである点にある。両者ともNMSを用いないため、検出数を左右する後処理上のしきい値は確信度しきい値confのみである。このため、検出数が変化した原因の切り分けが単純になる。
4.2 使用しているAIモデルの所在
- ソースコードの所在:Ultralyticsの公開リポジトリ(https://github.com/ultralytics/ultralytics)である。ライセンスはAGPL-3.0(改変物や通信越しに提供する場合にも同じ条件での公開を求めるオープンソースライセンス)およびEnterpriseライセンスである。
- 学習済みモデルの所在:ファイル名は
yolo26n.ptであり、ultralyticsパッケージが初回実行時に自動ダウンロードする。2回目以降は保存済みのファイルをそのまま使用する。本コードでは保存先を、実行時のカレントディレクトリではなく、プログラムファイルが置かれたディレクトリに固定している。 - 学習に用いたデータセット:COCO(Microsoft COCO 2017、80クラスの物体に注釈が付与された画像データセット)である。したがって検出できる物体はこの80クラスに限られる。
- 公表値:yolo26nは入力640画素でCOCO val上のmAP50-95が、既定の一対一ヘッドで40.1、NMSを併用する一対多ヘッドで40.9、パラメータ数2.4M、演算量5.4 GFLOPs(1回の推論で約54億回の浮動小数点演算)と報告されている。本コードは既定の一対一ヘッドを用いる。詳細はUltralytics公式ドキュメントおよびarXiv:2606.03748に記載されている。
4.3 注意点(手法としての説明と、実装であるコードとの違い)
- 劣化ガンマ変換の向きは実装によって異なる。本コードは
(画素値/255)**(1.0/gamma)*255を計算して四捨五入するため、gammaが1.00未満のときに画像が暗くなる。gammaが1.00のときは恒等変換となり、全256階調について変換前後の値が一致するため、上段は元フレームそのものである。逆向きの定義を用いる文献もあるため、他の資料と比較する際は定義を確認する必要がある。 - 本教材には由来の異なる2つのガンマ指数が現れる。1つは実験者が与える劣化ガンマ指数(行を決める独立変数)、もう1つはAGCWDが自動的に定める補正ガンマ指数γ(l)である。
- AGCWDが定めるのは階調ごとのγ(l)=1−cdfw(l)である。画面とログに表示するγ(l̄)は、入力画像の平均輝度に対応する階調におけるγ(l)の値であって、画像全体に共通のガンマ値ではない。
- 本コードのAGCWDはHSV色空間(色相H・彩度S・明度Vで色を表す表色系)のV成分に、CLAHEはCIELAB色空間のL*成分に適用する。RGB各成分に個別に適用する実装とは結果が異なる。
- 本コードのLIME型は簡略実装である。原論文が行う重み付き全変動正則化(画像の変化量の総和を抑えて平滑化する正則化)による最適化と、反射成分の雑音除去は実装していない。照明マップの初期値をガイデッドフィルタで平滑化し、指数γLで調整して除算するのみである。
- いずれの補正手法も、失われた階調を復元するものではない。劣化ガンマ変換によって同じ値に丸められた暗部の情報は戻らない。行うのは、残った階調の分布を引き伸ばす操作である。
- 劣化は照度低下の模擬であり、暗所での実測ではない。実際のカメラが低照度下で生じるショットノイズ(光子の到来数のゆらぎに由来する雑音)、露光時間の延長に伴うぶれ、ホワイトバランスの変動は再現されない。
- YOLO26の既定の推論経路はNMSを行わないため、NMSに関する調整項目は存在しない。
- 推論は補正後の画像に対して行われ、その内部で
imgszで指定した一辺への縮尺調整が施される。コード中のletterbox関数は15個のマスの寸法をそろえる表示用の処理であり、推論の入力には影響しない。
4.4 技術の限界
- 人手による正解ラベルを用意していないため、検出漏れと誤検出を機械的に区別できない。参照集合はモデル自身の出力であり、基準条件そのものに含まれる誤りは指標に反映されない。
- 学習済み重みはCOCOで学習されており、実験を行う教室や実験室に対する汎化性能(学習に用いなかったデータに対する性能)は測定されていない。
- 1フレームにつき15条件の推論を行うため、GPUを利用できない環境では画面の更新間隔が長くなる。
- 検出枠の位置の正しさは、基準条件との一致判定を通じてのみ扱う。真の物体位置に対する位置誤差は評価しない。
5. プログラムの概要
- 構成:単一のPythonファイルである。冒頭のコメントに、使用するAIモデルの名称、出典(論文)、学習済み重みのファイル名、学習に用いたデータセット名、併用する各画像処理手法の出典を記載している。本体は、劣化処理・補正処理・測定・描画を行う関数群と、画面を構成する
Applicationクラスから成る。 - モデル取得:起動時に
yolo26n.ptを読み込む。ファイルが存在しない場合は自動ダウンロードし、以後は保存済みのファイルをそのまま使用する。保存先はプログラムファイルと同じディレクトリである。 - 入力:「1. 映像入力源」でカメラデバイス(デバイス番号0〜3)または動画ファイルを選ぶ。取得したフレームは、条件間・実行間の再現性を確保するため、縦横比を保ったまま幅640画素に正規化してから処理する。
- 処理:1フレームごとに、3水準の劣化ガンマ指数で劣化画像を作り、そのそれぞれに5種の補正条件を適用して計15枚を生成し、1回のバッチ推論(複数の画像をまとめて1回で推論する処理)でYOLO26nに与える。個別に15回呼び出すより処理時間が短く、条件間で推論設定が完全に一致することも保証される。
- 出力(画像):5列×3行・計15マスの格子として表示する。各マスは縦横比を保ったまま黒の余白で寸法をそろえ、検出枠に加えて、条件記号、劣化ガンマ指数、補正条件名(AGCWDを含む列はγ(l̄)を併記)、検出数、平均確信度、平均明度L*、参照一致数、参照再現率、参照適合率を日本語で重ね書きする。
- 出力(テキスト):記録対象フレームについて、パラメータ値の行と、15条件それぞれの測定値の行をコンソールへ出力する。明度標準偏差とクラス別の内訳を含む。同じ内容をCSVファイルへ追記できる。
- GUI:6つのパネル(映像入力源、照度条件、検出器の設定、補正手法の設定、測定値の記録、実行制御)と結果表示領域から成る。スライダーは処理中も操作でき、変更は次のフレームの処理から反映される。
- 実行トリガー:「撮影開始」で処理が始まる。「撮影停止」、またはフレームの取得失敗(動画の終端、カメラの取得終了)で停止する。ウィンドウを閉じるとカメラを解放して終了する。
6. プログラム実行手順
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:インストーラーによるインストール
- Python公式サイト(https://www.python.org/downloads/)にアクセスし、「Download Python 3.x.x」ボタンからWindows用インストーラーをダウンロードする。
- ダウンロードしたインストーラーを実行する。
- 初期画面の下部に表示される「Add python.exe to PATH」にチェックを入れてから「Customize installation」を選択する。このチェックを入れ忘れると、コマンドプロンプトから
pythonコマンドを実行できない。 - 「Install Python 3.xx for all users」にチェックを入れ、「Install」をクリックする。
インストールの確認
コマンドプロンプトで以下を実行する。
python --version
バージョン番号(例:Python 3.12.x)が表示されればインストール成功である。「'python' は、内部コマンドまたは外部コマンドとして認識されていません。」と表示される場合は、インストールが正常に完了していない。
Build Tools・CUDA Toolkit・PyTorch のインストール
本章では、C++ ビルドツール、NVIDIA CUDA Toolkit、PyTorch のインストールを行い、GPU を活用した機械学習プログラムを実行する環境を整える。扱う環境は、Windows 搭載パソコンである。
[Build Tools・CUDA Toolkit・PyTorch のインストール手順を見るには、ここをクリック]
Windows での Build Tools for Visual Studio 2026 のインストール
Build Tools for Visual Studio 2026 は、C++ ソースコードを Windows 用バイナリにコンパイルするための開発ツール群である。unsloth 等の一部 Python パッケージは、インストール時に C++ コードのビルドを必要とするため、これらのツールが必須となる。
以下のコマンドは、Build Tools が未インストールの場合は winget で新規インストールし、インストール済みの場合は setup.exe modify でコンポーネントを追加する(バージョンは変更しない)。
【インストールコマンドの実行方法】
管理者権限でコマンドプロンプトを起動する(手順:Windows キーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。そして、コマンド全体をコマンドプロンプトにコピー&ペーストする。
REM VC++ ランタイム
winget install --scope machine --id Microsoft.VCRedist.2015+.x64 -e --silent --disable-interactivity --force --accept-source-agreements --accept-package-agreements --override "/quiet /norestart"
REM ============================================================
REM Visual Studio Build Tools + Desktop development with C++
REM (VCTools、MSBuildTools、CMake連携、Clang、Windows 11 SDK)
REM ============================================================
REM 進行中のインストーラーを停止(ロック競合回避)
taskkill /F /IM vs_setup.exe /T >nul 2>&1
taskkill /F /IM vs_installer.exe /T >nul 2>&1
taskkill /F /IM vs_installerservice.exe /T >nul 2>&1
REM 未インストール時: winget で新規インストール
REM インストール済み時: setup.exe modify でコンポーネント追加(バージョンは変更しない)
winget list --id Microsoft.VisualStudio.BuildTools 2>nul | findstr /i "BuildTools" >nul 2>&1
if %ERRORLEVEL% EQU 0 (
for /f "usebackq delims=" %P in (`"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -products Microsoft.VisualStudio.Product.BuildTools -property installationPath`) do start /wait "" "C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe" modify --installPath "%P" --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Workload.MSBuildTools --add Microsoft.VisualStudio.Component.VC.CMake.Project --add Microsoft.VisualStudio.Component.VC.Llvm.Clang --add Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset --add Microsoft.VisualStudio.Component.Windows11SDK.26100 --includeRecommended --quiet --norestart --nocache
) else (
winget install --scope machine --id Microsoft.VisualStudio.BuildTools -e --silent --disable-interactivity --force --accept-source-agreements --accept-package-agreements --override "--quiet --wait --norestart --nocache --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended --add Microsoft.VisualStudio.Workload.MSBuildTools --add Microsoft.VisualStudio.Component.VC.CMake.Project --add Microsoft.VisualStudio.Component.VC.Llvm.Clang --add Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset --add Microsoft.VisualStudio.Component.Windows11SDK.26100"
)
REM 破損時の修復(任意、動作がおかしくなった場合)
REM "C:\Program Files (x86)\Microsoft Visual Studio\Installer\setup.exe" repair --installPath "C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools" --quiet --norestart
REM 導入確認(インストールパスが表示されれば正常)
"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -products * -requires Microsoft.VisualStudio.Workload.VCTools -property installationPath
上記のコマンドでは、Build Tools 本体と Visual C++ 再頒布可能パッケージをインストールし、続いて以下のコンポーネントを追加している。
- VCTools:C++ デスクトップ開発ワークロード(
--includeRecommendedにより、MSVC コンパイラ、C++ AddressSanitizer、vcpkg、CMake ツール、Windows 11 SDK 等の推奨コンポーネントが含まれる) - MSBuildTools:MSBuild によるビルドツールのワークロード
- VC.CMake.Project:Windows 向け C++ CMake ツール
- VC.Llvm.Clang:Windows 向け C++ Clang コンパイラ
- VC.Llvm.ClangToolset:MSBuild から Clang を使用するための clang-cl ツールセット
- Windows11SDK.26100:Windows 11 SDK(ビルド 10.0.26100)
追加のコンポーネントが必要になった場合は Visual Studio Installer で個別にインストールできる。
Windows での NVIDIA CUDA Toolkit のインストール
NVIDIA CUDA Toolkit は、NVIDIA GPU 上で計算を行うためのコンパイラ・ライブラリ群である。PyTorch や vLLM 等が GPU を利用するために必要となる。GPU を使用しない場合、この手順は不要である。
前提条件:NVIDIA GPU、NVIDIA ドライバ、Build Tools for Visual Studio もしくは Visual Studio が必要である。
インストール中の注意:他のウインドウは閉じておくこと。
【インストールコマンドの実行方法】
管理者権限でコマンドプロンプトを起動する(手順:Windows キーまたはスタートメニュー → cmd と入力 → 右クリック → 「管理者として実行」)。そして、コマンド全体をコマンドプロンプトにコピー&ペーストする。
REM NVIDIA CUDA Toolkit 12.8 をシステム領域にインストール
winget install --scope machine --id Nvidia.CUDA --version 12.8 -e --silent --disable-interactivity --force --uninstall-previous --accept-source-agreements --accept-package-agreements --override "-s -n"
REM 環境変数TEMP, TMPの設定(一時ファイルの保存先を短いパスに変更)
mkdir C:\TEMP
setx TEMP "C:\TEMP" /M
setx TMP "C:\TEMP" /M
環境変数 TEMP および TMP を C:\TEMP に変更しているのは、後続のインストール処理で長いパス名や空白を含むパス名がエラーの原因となる場合があるためである。
Windows での PyTorch のインストール
https://pytorch.org のインストールガイドに従い、自環境の CUDA バージョンに対応したコマンドを取得して実行する。CUDA バージョンは以下で確認できる。
nvcc --version
Python 3.12、CUDA 12.6 以上の場合は、管理者権限でコマンドプロンプトを起動し、以下を実行する。cu128 は CUDA 12.8 用のタグである。CUDA バージョンが異なる場合は、上記公式サイトで該当するタグを確認し、URL 末尾の cu128 を置き換えること。
pip install --no-user -U numpy torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128
以降の章では、必要に応じて題材に応じた必要なソフトウェアを追加する。
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 プログラムを解釈・実行するソフトウェア)を選択する必要がある.
- コマンドパレット(コマンド名で機能を呼び出す VS Code の入力欄)を開く(
Ctrl+Shift+P) Python: Select Interpreterと入力する
- 表示される一覧から,使用する Python(例:
C:\Program Files\Python312\python.exe)を選択する.
Python プログラム実行手順
[Windows での Python プログラム実行手順を見るには、ここをクリック]
Windows での Python 実行手順(Visual Studio Codeを使用)
プログラムファイルの作成と保存
- 左サイドバーの「エクスプローラー」アイコン(
Ctrl+Shift+E)をクリックする
- 「NO FOLDER OPENED」(作業対象フォルダが未選択の状態)と表示される場合は,「Open Folder」をクリックし,プログラムを保存するフォルダを選択する
続いて「フォルダを信用するか」を確認する画面(フォルダ内のコードを実行してよいか確認する VS Code の仕組み)が表示されるので,チェックして Yes を選択する
- フォルダ名の右側に表示される「新しいファイル」アイコンをクリックする
- ファイル名(例:
aitask.py.ファイル名は何でも良い)を入力しEnterを押す.拡張子は.py(Python ファイルを示す拡張子)とする
- 実行したいコードを選択し,
Ctrl+Cでコピーする.VS Code のエディタ領域にCtrl+Vで貼り付ける Ctrl+Sで保存する
プログラムの実行
- エディタ右上の三角形「▷」アイコン(Run Python File:現在開いている Python ファイルを実行するボタン)をクリックする.または,エディタ上で右クリックし「ターミナルで Python ファイルを実行」を選択する
- VS Code 下部のターミナル(コマンドの入出力を表示する画面)に,実行結果(
print関数の出力等)が表示される
- tkinter(Python 標準の GUI ライブラリ)のファイル選択ダイアログを使うプログラムを実行した場合は,ダイアログが開くので対象画像を選択する
- VS Code 下部のターミナルで実行結果を確認する.OpenCV ウィンドウ(OpenCV が画像を表示するために開く専用ウィンドウ)が開いた場合はそちらも確認する.OpenCV ウィンドウは,マウスクリックでウィンドウをアクティブ(操作対象の状態)にしてからキーを押すと終了する
必要なライブラリのインストール
- 管理者権限でコマンドプロンプトを起動する
(手順:Windowsキーまたはスタートメニュー →
cmdと入力 → 右クリック → 「管理者として実行」)。 - 次のコマンドを実行し、関連ライブラリを導入または更新する。YOLO26に対応したバージョンが必要であり、対応していないバージョンでは
yolo26n.ptを解決できない。pip install -U --no-user ultralytics
実行手順
- 下記のプログラムを
program.pyなどの名前で保存し、実行する。初回実行時は学習済み重みの自動ダウンロードが行われるため、インターネット接続が必要である。進捗はコンソールに表示される。 - ウィンドウが開いたら、「1. 映像入力源」でカメラデバイスまたは動画ファイルを選ぶ。カメラが1台の場合、デバイス番号は0である。
- 他のパラメータは初期値のままでよい。値の変更は開始後にも行える。
- 「撮影開始」を押す。カメラを使う場合は、COCOの80クラスに含まれる物体(person、chair、backpack、bottle、laptopなど)が画面に入るようにカメラを向ける。
- 左上のマス(R1C1)で目的の物体が検出されていることを確認する。これは元フレームそのものであり、参照集合を与える基準条件である。
- 「静止フレーム保持」にチェックを入れる。以降は同じフレームが繰り返し処理されるため、被写体の動きを排除してパラメータだけを変えられる。
- 第1列を上から下へ見て、劣化ガンマ指数の低下に伴う検出数と参照再現率の変化を追う。
- 同じ行を左から右へ見て、5つの補正条件による差を確認する。
- 「撮影停止」で停止する。ウィンドウを閉じると終了する。
処理が重い場合はimgszを320に下げると1フレームあたりの処理時間が短くなる。ただしimgszは検出結果そのものに影響するため、1回の観察の中では固定する。
7. GUIで設定可能なパラメータ
最上行の劣化ガンマ指数は基準条件として1.00に固定している。それ以外はすべて画面上で変更でき、処理中の変更は次のフレームから反映される。
| No. | 名称 | コード上の識別子 | 初期値 | 範囲(刻み) | 説明 |
|---|---|---|---|---|---|
| 1 | 映像入力源の種別 | source_variable | カメラデバイス | カメラデバイス/動画ファイル | 処理対象の映像の取得元である。動画ファイルを選ぶ場合は、あらかじめ「ファイルを選択」ボタンでファイルを指定する。 |
| 2 | カメラデバイス番号 | camera_index_variable | 0 | 0〜3(1) | OpenCVがカメラを識別する番号である。カメラが1台のときは0を指定する。 |
| 3 | 劣化ガンマ指数(中段の行) | gamma_mid_variable | 0.60 | 0.10〜1.00(0.05) | 中段の行に与える照度低下の強さである。1.00で元フレームと一致し、値を下げるほど暗くなる。 |
| 4 | 劣化ガンマ指数(下段の行) | gamma_low_variable | 0.30 | 0.10〜1.00(0.05) | 下段の行に与える照度低下の強さである。中段より小さい値を与えると、上段から下段へ向かって単調に暗くなる。 |
| 5 | YOLO26n 確信度しきい値 conf | conf_variable | 0.25 | 0.05〜0.90(0.05) | 検出として採用する確信度の下限である。下げると検出数と誤検出がともに増える。条件間の比較を成立させるため、1回の観察の中では固定する。 |
| 6 | YOLO26n 推論入力の一辺 imgsz(画素) | imgsz_variable | 640 | 320〜960(32) | 推論の際に画像を調整する一辺の画素数である。小さくすると1フレームの処理時間が短くなる一方、画面内で小さく写る物体の検出が難しくなる。刻みを32としているのは、モデルの構造上、入力の一辺が32の倍数である必要があるためである。 |
| 7 | 参照集合との一致判定 IoUしきい値 | iou_variable | 0.50 | 0.30〜0.90(0.05) | 基準条件の検出と当該条件の検出を同一物体とみなすためのIoUの下限である。上げるほど枠の位置ずれに厳しくなり、参照再現率と参照適合率が下がる。 |
| 8 | CLAHE クリップ制限値 clipLimit | clip_limit_variable | 2.0 | 0.5〜8.0(0.5) | タイルごとのヒストグラムを切り詰める高さである。大きくするとコントラストの拡大量が増え、同時に暗部の雑音の増幅量も増える。第2列と第4列に適用される。 |
| 9 | CLAHE タイル分割数(縦横共通) | tile_grid_variable | 8 | 2〜16(1) | 局所ヒストグラムを計算する格子の分割数である。大きくすると1タイルが小さくなり、局所的な明暗差に追随する度合いが増す。第2列と第4列に適用される。 |
| 10 | AGCWD 重み付け指数 α | agcwd_alpha_variable | 0.50 | 0.10〜1.00(0.05) | 輝度確率密度関数を重み付けする際のべき指数である。1.00に近いほど元の分布に近く、小さいほど分布が平坦化される。原論文の推奨値は0.5である。第3列と第4列に適用される。 |
| 11 | 照明マップ調整指数 γL | lime_gamma_variable | 0.80 | 0.40〜1.00(0.05) | 推定した照明マップTに対して T←TγL を施す際の指数である。1.00未満にすると照明マップの値が大きくなるため除算による増幅が抑えられる。第5列に適用される。 |
| 12 | ガイデッドフィルタ半径 r(画素) | lime_radius_variable | 15 | 3〜45(2) | 照明マップを構造保存平滑化する際の局所窓の半径である。大きくすると照明マップが滑らかになり、物体の輪郭が照明成分に混入しにくくなる一方、細かな照明むらへの追随が鈍る。正則化パラメータεはコード中の定数(0.01)である。第5列に適用される。 |
| 13 | コンソールへの出力 | console_output_variable | 有効 | 有効/無効 | 記録対象フレームの測定値をコンソールへ出力する。 |
| 14 | CSVファイルへの記録 | csv_output_variable | 無効 | 有効/無効 | 有効にすると、プログラムファイルと同じディレクトリのmeasurement_log.csvへ1条件1行で追記する。文字コードはUTF-8(BOM付き。BOMはファイル先頭に置く文字コード識別用の印)であり、表計算ソフトでそのまま開ける。 |
| 15 | 記録間隔(フレーム) | record_interval_variable | 20 | 1〜60(1) | 記録を行う間隔である。フレーム番号がこの値の倍数になったときに記録する。 |
| 16 | 静止フレーム保持 | hold_frame_variable | 無効 | 有効/無効 | 有効にすると、新しいフレームを取得せず、直前に取得したフレームを繰り返し処理する。被写体の動きを排除してパラメータの効果だけを観察するための機能である。 |
8. プログラムコード
"""
低照度化と輝度補正が物体検出モデルの検出結果に与える影響の測定
照度条件3水準 × 輝度補正条件5水準(計15条件)を同一フレームから生成し、
同一の物体検出モデルで一括測定する。15条件は常に同時に表示される。
使用するAIモデル
名称 : Ultralytics YOLO26n(物体検出モデル)
実装 : https://github.com/ultralytics/ultralytics
学習済み重み : yolo26n.pt
学習データセット : COCO
併用する画像処理
ガンマ変換、CLAHE、AGCWD、LIME型照明マップ推定、ガイデッドフィルタ
"""
import csv
import os
import time
import tkinter as tk
from collections import defaultdict, deque
from tkinter import filedialog, ttk
import cv2
import numpy as np
from PIL import Image, ImageDraw, ImageFont, ImageTk
from ultralytics import YOLO
# ----------------------------------------------------------------------
# 定数
# ----------------------------------------------------------------------
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MODEL_PATH = os.path.join(BASE_DIR, "yolo26n.pt")
CSV_PATH = os.path.join(BASE_DIR, "measurement_log.csv")
JP_FONT_PATH = r"C:\Windows\Fonts\meiryo.ttc"
PROCESS_WIDTH = 640
CELL_W = 280
CELL_H = 200
OVERLAY_FONT_SIZE = 10
GUIDED_FILTER_EPSILON = 0.01
UNDEFINED_MARK = "-"
METHOD_NONE = "補正なし"
METHOD_CLAHE = "CLAHE"
METHOD_AGCWD = "AGCWD"
METHOD_AGCWD_CLAHE = "AGCWD→CLAHE"
METHOD_LIME = "LIME型照明マップ推定"
CSV_HEADER = [
"frame", "elapsed_second", "degradation_gamma", "correction_method",
"agcwd_gamma_at_mean_level", "mean_lightness", "std_lightness",
"detection_count", "mean_confidence",
"matched_count", "reference_recall", "reference_precision",
"conf_threshold", "imgsz", "match_iou_threshold",
"clahe_clip_limit", "clahe_tile_grid", "agcwd_alpha",
"lime_illumination_gamma", "lime_guided_radius", "class_detail",
]
# 学習済み重みの読み込み
model = YOLO(MODEL_PATH)
_font_cache = {}
def japanese_font(size):
"""日本語フォントを取得する。同じ大きさのフォントは再利用する。"""
if size not in _font_cache:
_font_cache[size] = ImageFont.truetype(JP_FONT_PATH, size)
return _font_cache[size]
# ----------------------------------------------------------------------
# 劣化処理
# ----------------------------------------------------------------------
def normalize_width(image_bgr, width=PROCESS_WIDTH):
"""縦横比を保ったまま指定幅に正規化する。"""
source_h, source_w = image_bgr.shape[:2]
if source_w == width:
return image_bgr
height = max(1, int(round(source_h * width / source_w)))
return cv2.resize(image_bgr, (width, height), interpolation=cv2.INTER_AREA)
def build_gamma_lut(exponent):
"""
べき乗変換のルックアップテーブルを作る。
exponent = 1.0 のときは全256階調で恒等変換になる。
"""
values = (np.arange(256, dtype=np.float64) / 255.0) ** float(exponent) * 255.0
return np.rint(np.clip(values, 0.0, 255.0)).astype(np.uint8)
def apply_gamma_darkening(image_bgr, gamma):
"""
照度低下の模擬。
gamma = 1.00 で恒等変換、gamma < 1.00 で暗くなる。
"""
return cv2.LUT(image_bgr, build_gamma_lut(1.0 / float(gamma)))
# ----------------------------------------------------------------------
# 輝度補正処理
# ----------------------------------------------------------------------
def apply_clahe(image_bgr, clip_limit, tile_grid):
"""
CLAHE。
色相の変化を避けるため、CIELAB色空間のL*成分にのみ適用する。
"""
lab = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2LAB)
lightness, chroma_a, chroma_b = cv2.split(lab)
clahe = cv2.createCLAHE(
clipLimit=float(clip_limit),
tileGridSize=(int(tile_grid), int(tile_grid))
)
merged = cv2.merge((clahe.apply(lightness), chroma_a, chroma_b))
return cv2.cvtColor(merged, cv2.COLOR_LAB2BGR)
def apply_agcwd(image_bgr, alpha):
"""
AGCWD(重み付け分布付き適応ガンマ補正)。
注意:
輝度階調が1種類しかない画像では、重み付きCDF(累積分布関数)が段関数になり、
非ゼロ画素が255へ写る。これはコントラスト情報を増やす処理ではないため、
この場合は恒等変換として扱う。
"""
hsv = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2HSV)
value = hsv[:, :, 2].copy()
histogram = cv2.calcHist([value], [0], None, [256], [0, 256]).ravel()
total = float(histogram.sum())
if total <= 0.0:
return image_bgr.copy(), 1.0
# 輝度値が1種類だけの場合は、AGCWDの変換をかけずに恒等変換にする。
if np.count_nonzero(histogram) <= 1:
return image_bgr.copy(), 1.0
pdf = histogram / total
pdf_max = float(pdf.max())
pdf_min = float(pdf.min())
# 全階調の出現確率が同一の場合、重み付け式の分母が0になる。
# その場合も、分布はすでに平坦であるため恒等変換にする。
if pdf_max - pdf_min < 1e-12:
return image_bgr.copy(), 1.0
weighted_pdf = pdf_max * (((pdf - pdf_min) / (pdf_max - pdf_min)) ** float(alpha))
weighted_cdf = np.cumsum(weighted_pdf) / float(weighted_pdf.sum())
exponent = 1.0 - weighted_cdf
levels = np.arange(256, dtype=np.float64) / 255.0
table = np.rint(np.clip((levels ** exponent) * 255.0, 0.0, 255.0)).astype(np.uint8)
table[0] = 0
hsv[:, :, 2] = cv2.LUT(value, table)
mean_level = int(np.clip(round(float(value.mean())), 0, 255))
return cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR), float(exponent[mean_level])
def guided_filter(guide, target, radius, epsilon):
"""
ガイデッドフィルタ。
"""
kernel = (2 * int(radius) + 1, 2 * int(radius) + 1)
mean_guide = cv2.boxFilter(guide, cv2.CV_32F, kernel)
mean_target = cv2.boxFilter(target, cv2.CV_32F, kernel)
mean_guide_guide = cv2.boxFilter(guide * guide, cv2.CV_32F, kernel)
mean_guide_target = cv2.boxFilter(guide * target, cv2.CV_32F, kernel)
variance = mean_guide_guide - mean_guide * mean_guide
covariance = mean_guide_target - mean_guide * mean_target
coefficient_a = covariance / (variance + float(epsilon))
coefficient_b = mean_target - coefficient_a * mean_guide
mean_a = cv2.boxFilter(coefficient_a, cv2.CV_32F, kernel)
mean_b = cv2.boxFilter(coefficient_b, cv2.CV_32F, kernel)
return mean_a * guide + mean_b
def apply_lime(image_bgr, illumination_gamma, radius):
"""
LIME型照明マップ推定の簡略実装。
"""
image = image_bgr.astype(np.float32) / 255.0
initial_map = image.max(axis=2)
refined_map = guided_filter(
initial_map,
initial_map,
radius,
GUIDED_FILTER_EPSILON
)
refined_map = np.clip(refined_map, 1e-3, 1.0) ** float(illumination_gamma)
corrected = image / refined_map[:, :, None]
return np.clip(corrected * 255.0, 0.0, 255.0).astype(np.uint8)
def build_correction_set(degraded_bgr, parameters):
"""
1行分、すなわち同一の劣化画像に対する5条件を作る。
"""
clahe_image = apply_clahe(
degraded_bgr,
parameters["clip_limit"],
parameters["tile_grid"]
)
agcwd_image, agcwd_gamma = apply_agcwd(
degraded_bgr,
parameters["agcwd_alpha"]
)
agcwd_clahe_image = apply_clahe(
agcwd_image,
parameters["clip_limit"],
parameters["tile_grid"]
)
lime_image = apply_lime(
degraded_bgr,
parameters["lime_gamma"],
parameters["lime_radius"]
)
return [
(METHOD_NONE, degraded_bgr, None),
(METHOD_CLAHE, clahe_image, None),
(METHOD_AGCWD, agcwd_image, agcwd_gamma),
(METHOD_AGCWD_CLAHE, agcwd_clahe_image, agcwd_gamma),
(METHOD_LIME, lime_image, None),
]
# ----------------------------------------------------------------------
# 測定量の算出
# ----------------------------------------------------------------------
def lightness_statistics(image_bgr):
"""CIELABの明度L*を0〜100に換算し、平均と標準偏差を返す。"""
lightness = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2LAB)[:, :, 0].astype(np.float32)
lightness = lightness * (100.0 / 255.0)
return float(lightness.mean()), float(lightness.std())
def extract_detections(result):
"""検出結果から矩形、クラス番号、確信度を取り出す。"""
boxes = result.boxes
if boxes is None or len(boxes) == 0:
return {
"xyxy": np.zeros((0, 4), np.float32),
"cls": np.zeros((0,), np.int32),
"conf": np.zeros((0,), np.float32),
}
return {
"xyxy": boxes.xyxy.cpu().numpy().astype(np.float32),
"cls": boxes.cls.cpu().numpy().astype(np.int32),
"conf": boxes.conf.cpu().numpy().astype(np.float32),
}
def intersection_over_union(box, boxes):
"""1つの矩形と複数の矩形とのIoUを返す。"""
if len(boxes) == 0:
return np.zeros((0,), np.float32)
left = np.maximum(box[0], boxes[:, 0])
top = np.maximum(box[1], boxes[:, 1])
right = np.minimum(box[2], boxes[:, 2])
bottom = np.minimum(box[3], boxes[:, 3])
overlap = np.maximum(0.0, right - left) * np.maximum(0.0, bottom - top)
area_box = max(0.0, box[2] - box[0]) * max(0.0, box[3] - box[1])
area_boxes = (
np.maximum(0.0, boxes[:, 2] - boxes[:, 0])
* np.maximum(0.0, boxes[:, 3] - boxes[:, 1])
)
return overlap / (area_box + area_boxes - overlap + 1e-9)
def count_matches_with_reference(detection, reference, iou_threshold):
"""
基準条件の検出集合を参照集合として、
クラス一致かつIoUしきい値以上の検出を1対1対応させる。
注意:
IoUの大きい順に貪欲に対応づけると、対応の順序によって
参照一致数が過小評価される場合がある。
そこで二部グラフの最大マッチング
(2つの集合の間で重複なく作れる組の最大数を求める方法)により、
成立可能な1対1対応数を数える。
"""
left_count = len(detection["cls"])
right_count = len(reference["cls"])
adjacency = [[] for _ in range(left_count)]
for detection_index in range(left_count):
candidates = np.where(
reference["cls"] == detection["cls"][detection_index]
)[0]
ious = intersection_over_union(
detection["xyxy"][detection_index],
reference["xyxy"][candidates]
)
valid_mask = ious >= float(iou_threshold)
valid_candidates = candidates[valid_mask]
valid_ious = ious[valid_mask]
order = np.argsort(-valid_ious)
adjacency[detection_index] = (
valid_candidates[order].astype(np.int32).tolist()
)
pair_left = np.full(left_count, -1, dtype=np.int32)
pair_right = np.full(right_count, -1, dtype=np.int32)
distance = np.zeros(left_count, dtype=np.int32)
infinity = left_count + right_count + 1
def bfs():
queue = deque()
found_free_reference = False
for left_index in range(left_count):
if pair_left[left_index] == -1:
distance[left_index] = 0
queue.append(left_index)
else:
distance[left_index] = infinity
while queue:
left_index = queue.popleft()
for right_index in adjacency[left_index]:
next_left = int(pair_right[right_index])
if next_left == -1:
found_free_reference = True
elif distance[next_left] == infinity:
distance[next_left] = distance[left_index] + 1
queue.append(next_left)
return found_free_reference
def dfs(left_index):
for right_index in adjacency[left_index]:
next_left = int(pair_right[right_index])
if next_left == -1 or (
distance[next_left] == distance[left_index] + 1
and dfs(next_left)
):
pair_left[left_index] = right_index
pair_right[right_index] = left_index
return True
distance[left_index] = infinity
return False
matched_count = 0
while bfs():
for left_index in range(left_count):
if pair_left[left_index] == -1 and dfs(left_index):
matched_count += 1
return int(matched_count)
def summarize_per_class(detection, names):
"""クラスごとの件数と平均確信度を返す。"""
collected = defaultdict(list)
for class_id, confidence in zip(detection["cls"], detection["conf"]):
collected[names[int(class_id)]].append(float(confidence))
return {
name: (len(values), sum(values) / len(values))
for name, values in collected.items()
}
def format_ratio(numerator, denominator):
"""比率を文字列にする。分母が0の場合は未定義記号を返す。"""
if denominator == 0:
return UNDEFINED_MARK
return f"{numerator / denominator:.2f}"
def format_mean_confidence(detection):
"""平均確信度を文字列にする。検出0件の場合は未定義記号を返す。"""
if len(detection["conf"]) == 0:
return UNDEFINED_MARK
return f"{float(detection['conf'].mean()):.3f}"
# ----------------------------------------------------------------------
# 表示用の描画
# ----------------------------------------------------------------------
def letterbox(image_bgr, width, height):
"""
縦横比を保ったまま指定サイズの枠内に収め、
余白を黒で埋めて中央に置く。
"""
source_h, source_w = image_bgr.shape[:2]
scale = min(width / source_w, height / source_h)
new_w = max(1, int(round(source_w * scale)))
new_h = max(1, int(round(source_h * scale)))
canvas = np.zeros((height, width, 3), dtype=np.uint8)
x_offset = (width - new_w) // 2
y_offset = (height - new_h) // 2
resized = cv2.resize(image_bgr, (new_w, new_h))
canvas[y_offset:y_offset + new_h, x_offset:x_offset + new_w] = resized
return canvas
def draw_text_lines(image_bgr, lines, font_size=OVERLAY_FONT_SIZE):
"""
日本語文字列をPillowで描画してBGR画像に戻す。
"""
pil_image = Image.fromarray(cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(pil_image)
font = japanese_font(font_size)
line_height = int(font_size * 1.4)
draw.rectangle(
[0, 0, pil_image.width, line_height * len(lines) + 4],
fill=(0, 0, 0)
)
for index, line in enumerate(lines):
draw.text(
(4, 2 + index * line_height),
line,
font=font,
fill=(0, 255, 0)
)
return cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
# ----------------------------------------------------------------------
# アプリケーション
# ----------------------------------------------------------------------
class Application:
def __init__(self, root):
self.root = root
self.root.title("低照度化と輝度補正が物体検出モデル(YOLO26n)の検出結果に与える影響の測定")
self.capture = None
self.running = False
self.file_path = ""
self.frame_index = 0
self.held_frame = None
self.photo_image = None
self.start_time = time.time()
self.build_upper_panels()
self.build_lower_panels()
self.build_result_area()
# ---- 画面の構築 ---------------------------------------------------
def build_upper_panels(self):
container = ttk.Frame(self.root)
container.grid(row=0, column=0, sticky="ew", padx=8, pady=4)
source_frame = ttk.LabelFrame(container, text="1. 映像入力源")
source_frame.grid(row=0, column=0, sticky="nsew", padx=4)
self.source_variable = tk.StringVar(value="camera")
ttk.Radiobutton(
source_frame,
text="カメラデバイス",
variable=self.source_variable,
value="camera"
).grid(row=0, column=0, sticky="w", padx=4)
ttk.Label(source_frame, text="デバイス番号").grid(row=0, column=1, sticky="e")
self.camera_index_variable = tk.IntVar(value=0)
ttk.Spinbox(
source_frame,
from_=0,
to=3,
width=4,
state="readonly",
textvariable=self.camera_index_variable
).grid(row=0, column=2, sticky="w", padx=4)
ttk.Radiobutton(
source_frame,
text="動画ファイル",
variable=self.source_variable,
value="file"
).grid(row=1, column=0, sticky="w", padx=4)
ttk.Button(
source_frame,
text="ファイルを選択",
command=self.choose_file
).grid(row=1, column=1, columnspan=2, sticky="w", padx=4)
self.file_label = ttk.Label(source_frame, text="(未選択)")
self.file_label.grid(row=2, column=0, columnspan=3, sticky="w", padx=6, pady=2)
illumination_frame = ttk.LabelFrame(container, text="2. 照度条件(独立変数)")
illumination_frame.grid(row=0, column=1, sticky="nsew", padx=4)
self.gamma_mid_variable = tk.DoubleVar(value=0.60)
self.gamma_low_variable = tk.DoubleVar(value=0.30)
self.add_scale(
illumination_frame,
0,
0,
"劣化ガンマ指数(中段の行)",
self.gamma_mid_variable,
0.10,
1.00,
0.05
)
self.add_scale(
illumination_frame,
1,
0,
"劣化ガンマ指数(下段の行)",
self.gamma_low_variable,
0.10,
1.00,
0.05
)
ttk.Label(
illumination_frame,
text="上段は基準条件として 1.00 に固定される"
).grid(row=4, column=0, sticky="w", padx=6, pady=2)
detector_frame = ttk.LabelFrame(container, text="3. 検出器の設定(統制変数)")
detector_frame.grid(row=0, column=2, sticky="nsew", padx=4)
self.conf_variable = tk.DoubleVar(value=0.25)
self.imgsz_variable = tk.IntVar(value=640)
self.iou_variable = tk.DoubleVar(value=0.50)
self.add_scale(
detector_frame,
0,
0,
"YOLO26n 確信度しきい値 conf",
self.conf_variable,
0.05,
0.90,
0.05
)
self.add_scale(
detector_frame,
1,
0,
"YOLO26n 推論入力の一辺 imgsz(画素)",
self.imgsz_variable,
320,
960,
32
)
self.add_scale(
detector_frame,
2,
0,
"参照集合との一致判定 IoUしきい値",
self.iou_variable,
0.30,
0.90,
0.05
)
def build_lower_panels(self):
container = ttk.Frame(self.root)
container.grid(row=1, column=0, sticky="ew", padx=8, pady=4)
correction_frame = ttk.LabelFrame(container, text="4. 補正手法の設定(処置)")
correction_frame.grid(row=0, column=0, sticky="nsew", padx=4)
self.clip_limit_variable = tk.DoubleVar(value=2.0)
self.tile_grid_variable = tk.IntVar(value=8)
self.agcwd_alpha_variable = tk.DoubleVar(value=0.50)
self.lime_gamma_variable = tk.DoubleVar(value=0.80)
self.lime_radius_variable = tk.IntVar(value=15)
self.add_scale(
correction_frame,
0,
0,
"CLAHE クリップ制限値 clipLimit(第2列・第4列)",
self.clip_limit_variable,
0.5,
8.0,
0.5
)
self.add_scale(
correction_frame,
0,
1,
"CLAHE タイル分割数(縦横共通、第2列・第4列)",
self.tile_grid_variable,
2,
16,
1
)
self.add_scale(
correction_frame,
1,
0,
"AGCWD 重み付け指数 α(第3列・第4列)",
self.agcwd_alpha_variable,
0.10,
1.00,
0.05
)
self.add_scale(
correction_frame,
1,
1,
"照明マップ調整指数 γ_L(第5列)",
self.lime_gamma_variable,
0.40,
1.00,
0.05
)
self.add_scale(
correction_frame,
2,
0,
"ガイデッドフィルタ半径 r(画素、第5列)",
self.lime_radius_variable,
3,
45,
2
)
record_frame = ttk.LabelFrame(container, text="5. 測定値の記録")
record_frame.grid(row=0, column=1, sticky="nsew", padx=4)
self.console_output_variable = tk.BooleanVar(value=True)
self.csv_output_variable = tk.BooleanVar(value=False)
self.record_interval_variable = tk.IntVar(value=20)
ttk.Checkbutton(
record_frame,
text="コンソールへ出力する",
variable=self.console_output_variable
).grid(row=0, column=0, sticky="w", padx=6, pady=2)
ttk.Checkbutton(
record_frame,
text="CSVファイルへ記録する",
variable=self.csv_output_variable
).grid(row=1, column=0, sticky="w", padx=6, pady=2)
ttk.Label(
record_frame,
text=f"記録先: {os.path.basename(CSV_PATH)}"
).grid(row=2, column=0, sticky="w", padx=6)
ttk.Label(
record_frame,
text="記録間隔(フレーム)"
).grid(row=3, column=0, sticky="w", padx=6, pady=2)
ttk.Spinbox(
record_frame,
from_=1,
to=60,
width=5,
state="readonly",
textvariable=self.record_interval_variable
).grid(row=4, column=0, sticky="w", padx=6)
control_frame = ttk.LabelFrame(container, text="6. 実行制御")
control_frame.grid(row=0, column=2, sticky="nsew", padx=4)
self.start_button = ttk.Button(
control_frame,
text="撮影開始",
command=self.start
)
self.start_button.grid(row=0, column=0, padx=4, pady=4, sticky="w")
self.stop_button = ttk.Button(
control_frame,
text="撮影停止",
command=self.stop,
state="disabled"
)
self.stop_button.grid(row=0, column=1, padx=4, pady=4, sticky="w")
self.hold_frame_variable = tk.BooleanVar(value=False)
ttk.Checkbutton(
control_frame,
text="静止フレーム保持(同一フレームを反復処理する)",
variable=self.hold_frame_variable
).grid(row=1, column=0, columnspan=2, sticky="w", padx=6, pady=2)
self.status_variable = tk.StringVar(value="待機中")
ttk.Label(
control_frame,
textvariable=self.status_variable,
wraplength=340
).grid(row=2, column=0, columnspan=2, sticky="w", padx=6, pady=4)
def add_scale(self, parent, row, column, text, variable, minimum, maximum, step):
ttk.Label(parent, text=text).grid(row=row * 2, column=column, sticky="w", padx=6)
tk.Scale(
parent,
from_=minimum,
to=maximum,
resolution=step,
orient="horizontal",
length=300,
variable=variable
).grid(row=row * 2 + 1, column=column, sticky="w", padx=6)
def build_result_area(self):
frame = ttk.LabelFrame(self.root, text="7. 測定結果(15条件を同時表示)")
frame.grid(row=2, column=0, sticky="nsew", padx=8, pady=6)
ttk.Label(
frame,
text=(
"列(左から):補正なし/CLAHE/AGCWD/AGCWD→CLAHE/"
"LIME型照明マップ推定 "
"行(上から):劣化ガンマ指数 1.00(基準条件)/中段/下段"
)
).grid(row=0, column=0, sticky="w", padx=4, pady=2)
self.image_label = ttk.Label(frame)
self.image_label.grid(row=1, column=0)
self.show(np.zeros((CELL_H * 3, CELL_W * 5, 3), dtype=np.uint8))
# ---- 操作 ---------------------------------------------------------
def choose_file(self):
path = filedialog.askopenfilename(
title="動画ファイルを選択してください",
filetypes=[("動画ファイル", "*.mp4 *.avi *.mov *.mkv *.wmv")],
)
if path == "":
return
self.file_path = path
self.file_label.configure(text=os.path.basename(path))
self.source_variable.set("file")
def start(self):
if self.source_variable.get() == "camera":
self.capture = cv2.VideoCapture(
self.camera_index_variable.get(),
cv2.CAP_DSHOW
)
elif self.file_path == "":
self.status_variable.set("動画ファイルが選択されていない")
return
else:
self.capture = cv2.VideoCapture(self.file_path)
if not self.capture.isOpened():
self.capture.release()
self.capture = None
self.status_variable.set("映像入力源を開けない。デバイス番号またはファイルを確認する")
return
self.frame_index = 0
self.held_frame = None
self.start_time = time.time()
self.running = True
self.start_button.configure(state="disabled")
self.stop_button.configure(state="normal")
self.status_variable.set("処理中")
self.update_frame()
def stop(self):
self.running = False
if self.capture is not None:
self.capture.release()
self.capture = None
self.start_button.configure(state="normal")
self.stop_button.configure(state="disabled")
self.status_variable.set("停止")
def close(self):
self.running = False
if self.capture is not None:
self.capture.release()
self.root.destroy()
# ---- 1フレーム分の処理 ---------------------------------------------
def update_frame(self):
if not self.running:
return
if self.hold_frame_variable.get() and self.held_frame is not None:
frame = self.held_frame.copy()
else:
received, frame = self.capture.read()
if not received:
self.stop()
self.status_variable.set("フレームを取得できない(動画の終端、またはカメラの取得終了)")
return
frame = normalize_width(frame)
self.held_frame = frame.copy()
self.frame_index += 1
self.process(frame)
self.root.after(1, self.update_frame)
def read_parameters(self):
return {
"conf": float(self.conf_variable.get()),
"imgsz": int(self.imgsz_variable.get()),
"match_iou": float(self.iou_variable.get()),
"gamma_mid": float(self.gamma_mid_variable.get()),
"gamma_low": float(self.gamma_low_variable.get()),
"clip_limit": float(self.clip_limit_variable.get()),
"tile_grid": int(self.tile_grid_variable.get()),
"agcwd_alpha": float(self.agcwd_alpha_variable.get()),
"lime_gamma": float(self.lime_gamma_variable.get()),
"lime_radius": int(self.lime_radius_variable.get()),
}
def process(self, frame):
started = time.time()
parameters = self.read_parameters()
gamma_levels = [
1.00,
parameters["gamma_mid"],
parameters["gamma_low"]
]
inference_inputs = []
condition_meta = []
for row_index, gamma in enumerate(gamma_levels):
degraded = apply_gamma_darkening(frame, gamma)
for column_index, (method, image, agcwd_gamma) in enumerate(
build_correction_set(degraded, parameters)
):
inference_inputs.append(image)
condition_meta.append(
(row_index, column_index, gamma, method, agcwd_gamma)
)
results = model.predict(
inference_inputs,
imgsz=parameters["imgsz"],
conf=parameters["conf"],
verbose=False
)
detections = [extract_detections(result) for result in results]
reference = detections[0]
reference_count = len(reference["cls"])
names = results[0].names
elapsed = time.time() - self.start_time
interval = max(1, int(self.record_interval_variable.get()))
should_record = self.frame_index % interval == 0
cells = [[None] * 5 for _ in range(3)]
console_lines = []
csv_rows = []
for index, (row_index, column_index, gamma, method, agcwd_gamma) in enumerate(condition_meta):
detection = detections[index]
count = len(detection["cls"])
matched = count_matches_with_reference(
detection,
reference,
parameters["match_iou"]
)
mean_lightness, std_lightness = lightness_statistics(
inference_inputs[index]
)
per_class = summarize_per_class(detection, names)
class_detail = "; ".join(
f"{name}: n={number}, 平均確信度={mean:.3f}"
for name, (number, mean) in sorted(per_class.items())
) or "検出なし"
label = (
method
if agcwd_gamma is None
else f"{method}(γ(l̄)={agcwd_gamma:.2f})"
)
recall_text = format_ratio(matched, reference_count)
precision_text = format_ratio(matched, count)
confidence_text = format_mean_confidence(detection)
overlay = [
f"R{row_index + 1}C{column_index + 1} 劣化γ={gamma:.2f}/{label}",
f"検出数n={count} 平均確信度={confidence_text} 平均明度L*={mean_lightness:.1f}",
f"参照一致数m={matched} 参照再現率={recall_text} 参照適合率={precision_text}",
]
cell = letterbox(results[index].plot(), CELL_W, CELL_H)
cells[row_index][column_index] = draw_text_lines(cell, overlay)
console_lines.append(
f" R{row_index + 1}C{column_index + 1} 劣化γ={gamma:.2f} {label} "
f"平均明度L*={mean_lightness:.1f} 明度標準偏差={std_lightness:.1f} "
f"n={count} 平均確信度={confidence_text} m={matched} "
f"参照再現率={recall_text} 参照適合率={precision_text} | {class_detail}"
)
if should_record:
csv_rows.append([
self.frame_index,
f"{elapsed:.3f}",
f"{gamma:.2f}",
method,
"" if agcwd_gamma is None else f"{agcwd_gamma:.4f}",
f"{mean_lightness:.2f}",
f"{std_lightness:.2f}",
count,
"" if count == 0 else f"{float(detection['conf'].mean()):.4f}",
matched,
"" if reference_count == 0 else f"{matched / reference_count:.4f}",
"" if count == 0 else f"{matched / count:.4f}",
f"{parameters['conf']:.2f}",
parameters["imgsz"],
f"{parameters['match_iou']:.2f}",
f"{parameters['clip_limit']:.1f}",
parameters["tile_grid"],
f"{parameters['agcwd_alpha']:.2f}",
f"{parameters['lime_gamma']:.2f}",
parameters["lime_radius"],
class_detail,
])
self.show(np.vstack([np.hstack(row) for row in cells]))
if should_record:
if self.console_output_variable.get():
self.print_measurement(parameters, console_lines, reference_count)
if self.csv_output_variable.get():
self.write_csv(csv_rows)
duration = (time.time() - started) * 1000.0
self.status_variable.set(
f"フレーム {self.frame_index} 1フレームの処理時間 {duration:.0f} ms"
f"(15条件のバッチ推論を含む) 参照集合の要素数 {reference_count}"
)
def print_measurement(self, parameters, console_lines, reference_count):
print(
f"[frame={self.frame_index:06d}] conf={parameters['conf']:.2f} "
f"imgsz={parameters['imgsz']} 一致判定IoU={parameters['match_iou']:.2f} "
f"劣化γ中段={parameters['gamma_mid']:.2f} "
f"劣化γ下段={parameters['gamma_low']:.2f} "
f"clipLimit={parameters['clip_limit']:.1f} "
f"タイル分割数={parameters['tile_grid']}x{parameters['tile_grid']} "
f"AGCWD_α={parameters['agcwd_alpha']:.2f} "
f"照明マップ調整指数={parameters['lime_gamma']:.2f} "
f"ガイデッドフィルタ半径={parameters['lime_radius']} "
f"| 参照集合の要素数={reference_count}"
)
for line in console_lines:
print(line)
def write_csv(self, csv_rows):
if not csv_rows:
return
need_header = (
not os.path.exists(CSV_PATH)
or os.path.getsize(CSV_PATH) == 0
)
with open(CSV_PATH, "a", newline="", encoding="utf-8") as handle:
if need_header:
handle.write("\ufeff")
writer = csv.writer(handle)
if need_header:
writer.writerow(CSV_HEADER)
writer.writerows(csv_rows)
def show(self, image_bgr):
pil_image = Image.fromarray(cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB))
self.photo_image = ImageTk.PhotoImage(pil_image)
self.image_label.configure(image=self.photo_image)
window = tk.Tk()
application = Application(window)
window.protocol("WM_DELETE_WINDOW", application.close)
window.mainloop()
9. ヒントと考察ポイント
9.1 ヒント
- 左上のマス(R1C1)は元フレームそのものであり、参照集合を与える基準条件である。ここで検出されない物体は、暗さの影響ではなく、そもそも検出対象になっていない。まずR1C1で目的の物体が検出される状態を作ってから、他のマスを見る。
- R1C1は参照集合そのものであるから、検出が1つ以上あれば、参照再現率と参照適合率は定義上つねに1.00となる(検出が0個のときは分母が0となり「-」と表示される)。ここが1.00でなければ読み方を誤っている。
- 「静止フレーム保持」を有効にすると同じフレームが繰り返し処理されるため、被写体の動きを排除してパラメータの効果だけを観察できる。動画ファイルを入力とする場合も同様に、直前に取得した1枚のフレームが繰り返し処理される。
- 検出数と平均確信度は必ず組にして読む。検出数が減ると平均をとる母数も減るため、平均確信度がほとんど下がらないまま検出数だけが落ちることがある。
- 検出数が増えたことを直ちに改善と解釈しない。参照適合率が同時に下がっていれば、増えた分は基準条件になかった検出である。
- 表示の「-」は、分母が0で値が定義されないことを表す。0という測定値ではない。
- 画面の重ね書きは要点のみである。明度標準偏差とクラス別の内訳は、コンソール出力とCSVに含まれる。CSV記録を有効にすると、表計算ソフトで条件別に並べて比較できる。
9.2 実験のバリエーション
- 下段の劣化ガンマ指数を1.00から0.10へ0.05刻みで下げ、第1列の検出数が落ちる値を絞り込む。中段は動かさず、比較のために残す。
- 確信度しきい値confを0.05から0.90まで動かし、同じ暗さに対する検出数の変化を見る。
- clipLimitを0.5から8.0まで、タイル分割数を2、8、16と変え、CLAHEの強さと検出結果の関係を見る。
- AGCWD重み付け指数αを0.10から1.00まで動かし、第3列に表示されるγ(l̄)と検出結果の変化を見る。
- 照明マップ調整指数γLとガイデッドフィルタ半径rを動かし、第5列の変化を見る。
- 推論入力の一辺imgszを320、640、960に変え、解像度と検出結果、および画面の更新間隔の関係を見る。
9.3 考察ポイント
- 劣化ガンマ指数を連続的に下げたとき、検出数は急に0になるのか、段階的に減るのか。急に消えるのであれば、検出の可否を分けるしきい値的な挙動がどこかにあることになる。
- 補正によって検出数や参照再現率が上がる条件と下がる条件の両方が現れるか。最上行(劣化なし)で下がる手法があれば、前処理は常に有効とは限らないことになる。
- 平均明度L*を最も高くする手法と、参照再現率を最も高くする手法は一致するか。一致しないのであれば、明るくすることと検出しやすくすることは別の目標である。
- 第1列と第2列の差(補正なしにCLAHEを加えた効果)と、第3列と第4列の差(AGCWDにCLAHEを加えた効果)は同じ大きさか。後者が小さい、または負であれば、2つの補正の効果は加算的ではなく、AGCWDがすでに階調を広げた画像にCLAHEを重ねても得られる利得は小さい、あるいは逆効果であることになる。
- 入力が暗いほどγ(l̄)は小さくなるか。そうであれば、AGCWDは補正の強さを照度に応じて自動調整していることになる。
- 同じ劣化ガンマ指数でも、物体のクラスによって消えやすさに差があるか。差があれば、輝度低下の影響が物体の大きさ、形状、色にも依存すると考えられる。
- confを下げたときに暗い行の検出数が回復するか。回復するのであれば、「検出できなかった」のではなく「確信度がしきい値を下回った」と解釈でき、検出数という指標がしきい値の設定と切り離せないことが確かめられる。