ケイエルブイは、ハイパースペクトルカメラ・光学部品・光源など世界中の光学機器を取り扱う専門商社です。

03-3258-1238

お問い合わせ
KLV大学 ハイパースペクトルカメラコース
spectral-analysis-python-top.jpg

Python×スペクトル解析
[1.4]スペクトルビューアーの作成

スペクトル解析を始めるには、可視化が重要な役割を果たします。
スペクトル画像の表示や各画素のスペクトルの表示ができる簡易ビューアがあると、前処理や解析の当たり付けに活用できます。 そこで、本記事では、ここまでに紹介したSpectral PythonやMatplotlibによる、スペクトルデータの読み込み、スペクトル画像表示、スペクトルグラフ表示を基礎に、GUIの構築が可能なtkinterを持ちいて簡易的なビューアの実装方法を解説します。

スペクトル解析のPythonプログラム_基本フロー

次の章では前処理や多変量解析などのスペクトル解析プログラムの構築方法に進んでいきます。
その前に、本ビューアの編集に必要な要点を素早く押さえておくことで、後続のスペクトル解析機能の実装や自動化へスムーズに発展させるための足場として活用してください。

今回の「スペクトル解析×Python」企画の中でも少し複雑な内容になりますので、もし難しく感じられた場合は、そのまま次の章へ進んでいただいて大丈夫です。

スペクトルビューアー完成イメージ

プログラムの内容を説明する前に、今回作成するスペクトルビューアーの内容を紹介します。

スペクトルビューアーの完成イメージ動画

サンプルプログラムダウンロード

今回解説するプログラムは以下からダウンロード可能です。

ダウンロード

サンプルプログラムをダウンロードした後、次の手順で実行できます:

  1. Pythonのインストール
  2. Python をインストールしていない場合は、下記の解説記事を参照してインストールしてください。
    Python のインストール手順

  3. 必要なライブラリのインストール
  4. コマンドラインまたはターミナルで以下を実行します

    pip install spectral matplotlib numpy

  5. Pythonでプログラムを実行
  6. 以下のコマンドでプログラムを実行できます。

    python hyperspectral_viewer.py

    プログラムが起動すると、GUIが表示されます。

スペクトルビューアーの主な機能

動画からもわかりますが、以下の機能を有するスペクトルビューアーを作成していきます。

・スペクトルデータの読み込み

ファイルダイアログから選択したHDR形式(ENVI形式)のスペクトルデータで読み込みます。
選択されたファイルを正しく読み込むと、画像やスペクトルの表示が可能になります。

・スペクトル画像表示(グレイスケール画像/偽RGB画像)

読み込んだデータから、単一バンドのグレイスケール画像やヒートマップ、または偽RGB画像として表示できるようにします。

さらに、スライダーで実際の波長を見ながらバンドを選べるようにします。スライダーの値は、実際の波長から取得し、スライダーを動かすと、波長の表示もそれに合わせて更新します。

・スペクトルグラフを表示

画像内をクリックすると、その場所のスペクトル(波長毎の輝度値)を右側のグラフに追加します。
クリックした場所には”+”マーカーを表示することで、どのポイントのスペクトルを表示しているかをわかりやすくします。

また、クリックしたピクセルのスペクトルを色を変えて重ねて表示できます。凡例に座標を表示する事で、複数のピクセルのスペクトルを比較しやすくします。
最後に、リセットボタンでスペクトルとマーカーを一括で消去できるようにします。

ここからは、スペクトルビューアーのサンプルプログラムの内容について説明していきます。

ライブラリのセットアップ → GUIの構築 → データ読み込み → スペクトル画像表示 →スペクトルグラフ表示の順です。
サンプルプログラムと順番を合わせてありますので、見比べながらご確認ください。

1. ライブラリのセットアップ

プログラム作成の前段として、まず必要なライブラリ環境を整える必要があるので、ライブラリのセットアップに関して説明していきます。
セットアップは (1) 必要ライブラリの読み込み と (2) ライブラリの連携設定 の二段構えでの設定を行います。

(1) 必要ライブラリの読み込み

# === Setup ===============================================================

# read_leb
import os
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import numpy as np
import spectral
import matplotlib

プログラムで使用する主要なライブラリを読み込みます。
今回使用するライブラリとその役割は次のとおりです。

・tkinter / ttk

tkinterは、標準のPythonに含まれるGUIツールキットで、今回のGUI化のメインとなるライブラリです。
tkinter は土台となるウィンドウやイベントループ、ttk がウィジェット群(ボタン、スライダーなど)を提供してくれます。
さらに filedialog はHDRファイルを選ぶダイアログ、messagebox はエラー表示に使います。

・NumPy(numpy)

多次元配列の数値処理を得意とするライブラリです。高速で信頼性が高いので、ハイパースペクトルのような大きい配列でも処理することができます。

・Spectral Python(spectral)

ハイパースペクトル向けの定番ライブラリで、ENVI形式のスペクトルイメージの読み込みに使用します。
詳しくは、「[1.1] Pythonでスペクトルデータを読み込んでみる」をご確認ください。

・Matplotlib(matplotlib)

画像の埋め込み表示や、スペクトルグラフの描画に利用するライブラリです。後述の設定で、Tkinterウィンドウに自然に組み込めるようにします。
詳しくは、「1.2 スペクトルデータ画像表示」をご確認ください。

(2) ライブラリの連携設定 (Matplotlib と Tkinter の連携設定)

# Integration Setup
matplotlib.use("TkAgg")

from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
import matplotlib.pyplot as plt

次に 、Matplotlib を Tkinter と接続するための設定とクラスの読み込みを行います。

・matplotlib.use("TkAgg")

Matplotlibの描画先を Tkinter用(TkAgg) にするために必要な設定です。

・from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

Matplotlib の Figure を Tk ウィジェットとして埋め込むための設定です。これでスペクトル画像やスペクトルグラフを Tk ウィンドウ内の各Figureに表示できます。

・NavigationToolbar2Tk

埋め込んだグラフに ズーム・パン・保存 などのツールバーを追加するための設定です。スペクトルを拡大して細部を確認したいときに便利です。

・from matplotlib.figure import Figure

Matplotlibの図(Figure)を作るための設定です。生成した Figure に Axes を載せてグラフを構成し、Tk へ埋め込むことで出力します。

・import matplotlib.pyplot as plt

Matplotlib のAPIであるpyplotを使うための設定です。カラーマップ取得(plt.cm)、簡易プロット、イベント処理まわりのユーティリティとして利用します。

これで、プログラムで使用するライブラリ環境のセットアップは完了です。ここからは、GUIを構築していきます。

2. GUIの構築プログラム

ここではプログラムのGUI 構造を構築していきます。まずは、今回作成する画面のレイアウトについて解説します。

Spectral ViewerのGUI配置

今回は、主に実現したい以下の3つの機能でGUIを構成しました。
①上部:スペクトルデータ(ENVIファイル)の読み込み
②左部:スペクトル画像表示
③右側:スペクトルグラフ表示

プログラムの内容説明に関しては、GUI初期設定について説明したあと、各機能(メニュー 、データ読み込み用トップバー、左右の分離、スペクトル画像、スペクトルグラフ)のパーツの配置に関して行います。

(1) GUI初期設定

class HyperspecTk(tk.Tk):  # Tkのトップレベルウィンドウ
   def __init__(self):
       super().__init__()  # 初期化
       self.title("Hyperspectral Viewer (powered by KLV)")  # タイトル設定
       self.geometry("1350x850")  # Windowサイズ(幅×高さ)

まずは、ウィンドウのサイズ、どのようなパーツを作成するかを設定し、初期化を行います。

self.img = None  # spectral.open_imageで読み込んだデータ
self.data = None            # スペクトルデータ(H, W, B)
self.wavelengths = None     # 波長配列
self.gray_band = 0  # Gray画像表示の選択バンド
self.rgb_bands = {"R": None, "G": None, "B": None}  # 疑似RGB用の選択バンド
self.cmap_name = tk.StringVar(value="gray")  # カラーマップ名
self._snapping = False  # スライダーのスナップ処理での再入防止フラグ
self.points = []        # “+”マーカー用のクリック履歴

アプリの各情報を self の属性として保持するようにします。
self.data や self.points など同じインスタンスの属性にアクセスすることで、プログラム内で情報を共有することができるようになります。

self._build_menu()    # メニューバー
       self._build_topbar()  # 上部バー(ファイル読み込み)
       self._build_panes()   # 左右の分割
       self._build_left_tabs()  # 左側:スペクトル画像出力、Gray/偽RGBのTAB
       self._build_right_panel()  # 右側:スペクトルグラフ
       self._set_sliders_state(tk.DISABLED)  # ファイル未読込時のスライダー無効化

これらの記述は、この後に説明する GUI の各ビルダー(_build_menu、_build_topbar など)を __init__ 内で順に呼び出して 画面を構成する“司令塔”の役割を果たします。
メニュー → トップバー → 左右のパネル分割 → 左パネル → 右パネルの順に初期化し、最後に未読込時の誤操作を防ぐため スライダーを無効化 しています。

(2) GUI構造の構築

(2)-1 メニューバーの作成

起動時にメニューバーを作ります。

スペクトルビューアー_メニュー
# === UI Structure =======================================================
# -- builders --
def _build_menu(self):
    m = tk.Menu(self)  # ルートにメニューバー作成
    f = tk.Menu(m, tearoff=False)  # Fileメニューを作成
    f.add_command(label="Open HDR...", command=self.open_hdr)  # HDRを開くコマンド
    f.add_separator()  # 区切り線
    f.add_command(label="Exit", command=self.destroy)  # 終了コマンド
    m.add_cascade(label="File", menu=f)  # Fileメニューをバーに登録
    self.config(menu=m)  # ウィンドウにメニューを適用

まずルート用のMenuを生成し、その中に File サブメニューを作成します。
次に、File 内に①スペクトルデータの読み込み用の”Open HDR…”、②区切り線、③アプリを終了する Exitを順に登録します。

続いて add_cascade で File をメニューバーにぶら下げ、self.config(menu=m) でウィンドウへ適用します。これにより画面上部に標準的な File メニューが表示され、ファイル読み込みや終了の操作ができるようになります。

(2)-2 上部:スペクトルデータ(ENVIファイル)の読み込みパネル

ウィンドウ最上段にフル幅で、スペクトルデータの読み込みパネルを構築します。

スペクトルビューアー_メニュー
def _build_topbar(self):
    top = ttk.Frame(self)  # 上部フレーム作成(全幅)
    top.pack(fill=tk.X, padx=8, pady=(6, 4))  # 横一杯に配置
    ttk.Button(top, text="Open HDR...", command=self.open_hdr).pack(side=tk.LEFT)  # HDR選択ボタンの配置
    self.path_var = tk.StringVar(value="-")  # パス表示(初期は“-”)
    ttk.Label(top, textvariable=self.path_var).pack(side=tk.LEFT, padx=10)  # 選択中パスを表示

まず ttk.Frame を作成し、pack(fill=tk.X) で横方向にいっぱいに配置します。
左側に 「Open HDR…」 ボタンを置き、このボタンを押すと、 open_hdr() のコマンドを実行しファイル選択ダイアログを開きます。
続いて、 StringVarに、選択中のファイルパスであるself.path_varを設定し、ttk.Label の textvariable にバイドします。これにより、HDR を読み込み時に、self.path_var.set(...) によりラベルが更新され、選択したファイルのパスを表示できます。

(2)-3左右のパネルの分割

中央エリアを左右に分割する土台を作ります。

スペクトルビューアー_メニュー
def _build_panes(self):
    self.pw = ttk.PanedWindow(self, orient=tk.HORIZONTAL)  # Windowの水平分割
    self.pw.pack(fill=tk.BOTH, expand=True, padx=6, pady=(0, 6))  # 伸縮に追随
    self.left = ttk.Frame(self.pw)  # 左側Frame(画像表示用)
    self.right = ttk.Frame(self.pw)  # 右側Frame(スペクトルグラフ用)
    self.pw.add(self.left, weight=3)  # 左側Frameのウェイト設定(3)
    self.pw.add(self.right, weight=2)  # 右側Frameのウェイト設定(2)

ttk.PanedWindow(..., orient=tk.HORIZONTAL)で水平分割コンテナを生成し、pack(fill=tk.BOTH, expand=True, padx=6, pady=(0,6))でウィンドウのリサイズに追随するようにします。
左側には画像用事用のFrame(self.left)、右側にはスペクトル画像表示用のFrame(self.right)を作成し、self.pw.add(..., weight=3/2)で領域の配分比を左3:右2に設定しています。これによりユーザーは中心のバーをドラッグして幅を調整できるようになります。

(2)-4 左側パネル:スペクトル画像表示

左側のパネルに、Gray ImageとPseudo RGBの2種類の画像が表示できるよう、切り替えのためのタブを用意します。
Gray image には「波長スライダーと選択しているバンドの表示」、「color cmap を選ぶための Combobox」 を用意し、単バンド画像を Matplotlib キャンバスに描画します。
Pseudo RGB は 「R/G/B の3本の波長スライダーとそのバンドの表示」を用意し、RGB画像をMatplotlibキャンパスに描画します。
Gray Image、Psesdo RGBともに、画像をクリックした際には、その座標のスペクトルを右側のスペクトルグラフに追加する機構も加えます。
少し長くなりますが、順を追って紹介していきます。

スペクトルビューアー_メニュー
スペクトルビューアー_メニュー
①左側のコンテナ作成
def _build_left_tabs(self):
    self.nb = ttk.Notebook(self.left)  # タブコンテナ
    self.nb.pack(fill=tk.BOTH, expand=True, padx=6, pady=6)  # 伸縮に追随

ttk.Notebook を配置し、タブ型のコンテナとして機能させます
そして、pack(fill=BOTH, expand=True) により親フレームのリサイズに追従するようにします。

②Gray画像用タブ
# Gray tab
self.tab_gray = ttk.Frame(self.nb)  # Gray imageタブのフレーム
self.nb.add(self.tab_gray, text="Gray Image")  # タブへのラベル追加

左側のコンテナの上部にGray Image用フレームを追加し、タブラベルを「Gray Image」として登録します。

③Gray画像用波長スライダー
row = ttk.Frame(self.tab_gray); row.pack(fill=tk.X, padx=6, pady=(6, 0))  # スライダー行
ttk.Label(row, text="Wavelength:").pack(side=tk.LEFT)  # ラベル
self.gray_scale_var = tk.DoubleVar(value=0.0)  # スライダーの値(波長)
self.gray_scale = tk.Scale(
     row, variable=self.gray_scale_var, command=self.on_gray_scale,  # 変更時ハンドラ
    orient=tk.HORIZONTAL, length=500, width=16, sliderlength=18,  # 横スライダー
    from_=0.0, to=0.0, resolution=1.0, showvalue=True  # 範囲/分解能(後で実データに設定)
 )
self.gray_scale.pack(side=tk.LEFT, padx=(8, 12))  # スライダー配置
self.gray_sel_var = tk.StringVar(value="-")  # 選択表示用テキスト
ttk.Label(row, textvariable=self.gray_sel_var).pack(side=tk.LEFT)  # 現在のBand/λを表示

Gray ImageのFrame上段に“波長スライダー行”を構築します。
まずrow を横いっぱいに配置し、左に「Wavelength:」ラベルを表示。次に、横向き のスライダーを配置し、値を DoubleVar(gray_scale_var)で保持します。Wavelengthの範囲と間隔(from_/to_/resolution) は、初期には仮値で、HDR読込後に実波長範囲へ更新します。スライダーの操作時はcommand=on_gray_scale により最も近いバンドへスナップします。
右側には選択状態ラベル(gray_sel_var)を置いて、バンド数(Band i (λ=…)) が表示されるようにします。tk.Scaleにshowvalue=Trueを設定することで、スライダーのつまみの付近にも波長の数値が表示されます。

④Gray画像用カラーマップ選択プルダウン
row2 = ttk.Frame(self.tab_gray); row2.pack(fill=tk.X, padx=6, pady=6)  # cmap選択行
ttk.Label(row2, text="Colormap:").pack(side=tk.LEFT)  # ラベル
cmaps = ["gray", "viridis", "magma", "plasma", "inferno", "cividis", "jet", "turbo", "gnuplot2"]  # 候補
self.cmap_cb = ttk.Combobox(row2, state="readonly", values=cmaps, textvariable=self.cmap_name, width=12)  # 選択式
self.cmap_cb.pack(side=tk.LEFT, padx=6)  # 配置
self.cmap_cb.bind("<>", lambda e: self._update_gray_image())  # 変更で再描画

Grayタブ中段にカラーマップ選択用のプルダウンを作ります。
row2 を横いっぱいに配置し、左に「Colormap:」ラベルを表示します。
次にComboboxでプルダウンのBOXを配置し、gray/viridis/magma/plasma/inferno/cividis/jet/turbo/gnuplot2 をリストで定義することで、この中からColormapが選択できるようにします。

self.cmap_cb.bind("<>", ...)を設定することで、ユーザー操作によってttk.Combobox の選択が行われた際に、 <<ComboboxSelected>> が実行されます。
pack(side=LEFT, padx=6) はレイアウトの見た目を整えるための設定です。

⑤Gray画像表示
self.gray_fig = Figure(figsize=(6, 6), dpi=100)  # Gray用Figure作成
self.ax_gray = self.gray_fig.add_subplot(111)  # 1面のAxes
self.ax_gray.set_xticks([]); self.ax_gray.set_yticks([])  # 目盛り非表示
self.gray_canvas = FigureCanvasTkAgg(self.gray_fig, master=self.tab_gray)  # Tkに埋め込み
self.gray_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=6, pady=(2, 6))  # 余白、Windowリサイズ
self.gray_fig.canvas.mpl_connect("button_press_event", self.on_image_click)  # クリックイベント

Gray画像表示用に Matplotlibの Figure(figsize=(6,6), dpi=100)を作成し、その中にadd_subplot(111) で 1 つの描画領域(Axes)を用意します。
set_xticks([]); set_yticks([]) で目盛りを消して、FigureCanvasTkAgg で Gray画像(gray_fig)をTk ウィジェット化して受け込みます。
pack(fill=BOTH, expand=True, padx=6, pady=(2,6)) は、余白の設定と親Windowにリサイズに追随させるために必要です。。
self.gray_fig.canvas.mpl_connectで画像上のクリックを捕捉し、button_press_eventを発火させ、クリックした座標を on_image_click 渡すことで、画像上には “+” マーカーを即時描画するとともに、そのポイントのスペクトルグラフを描画させます。
(5章「(2)画像クリックによるスペクトルグラフ描画機能」で別途解説。)

⑥偽RGB画像タブ
# Pseudo RGB tab
self.tab_rgb = ttk.Frame(self.nb)  # RGBタブ用フレーム
self.nb.add(self.tab_rgb, text="Pseudo RGB")  # タブラベル追加

偽RGB用フレームを追加し、タブラベルを「Pseudo RGB」として登録します。

⑦偽RGB画像用波長スライダー
def make_rgb_slider(parent, label, var):
    r = ttk.Frame(parent); r.pack(fill=tk.X, padx=6, pady=4)  # スライダー行
    ttk.Label(r, text=f"{label} wavelength:").pack(side=tk.LEFT)  # R/G/Bラベル
    sc = tk.Scale(
        r, variable=var, command=self.on_rgb_scale, orient=tk.HORIZONTAL,  # 変更時ハンドラ
        length=600, width=16, sliderlength=18, from_=0.0, to=0.0, resolution=1.0, showvalue=True   )# 初期値:後で実値に設定
    sc.pack(side=tk.Left, padx=8)  # スライダー配置
    lv = tk.StringVar(value=f"{label}: -")  # 選択表示
    ttk.Label(r, textvariable=lv).pack(side=tk.LEFT)  # 現在値テキスト
    return sc, lv  # スライダーとラベル変数を返す

self.r_var, self.g_var, self.b_var = tk.DoubleVar(), tk.DoubleVar(), tk.DoubleVar()  # R/G/Bの値を取得
self.r_scale, self.r_label = make_rgb_slider(self.tab_rgb, "R", self.r_var)  # Rスライダー生成
self.g_scale, self.g_label = make_rgb_slider(self.tab_rgb, "G", self.g_var)  # Gスライダー生成
self.b_scale, self.b_label = make_rgb_slider(self.tab_rgb, "B", self.b_var)  # Bスライダー生成

偽RGBタブの上部にR/G/Bの3本の横スライダーを縦に並べます。

1本分のスライダーを作成する関数として、make_rgb_sliderを作成します。
この関数の中では、フレームを横一杯に配置し、左に「R/G/B wavelength:」ラベル、中央に スライダー(Scale)を置きます。from_/to_/resolutionには、Gray画像と同様に、読み込み前の仮値を設定しておき、HDR読込後に更新します。
右側の lv は表示用 StringVar で、選択中のバンド/波長を後段の _update_rgb_labels() で反映します。

あとは、R/G/B それぞれで”make_rgb_slider関数”を読み出して、3本のスライダーを配置します。

⑧偽RGB画像表示
self.rgb_fig = Figure(figsize=(6, 6), dpi=100)  # 偽RGB画像用Figure
self.ax_rgb = self.rgb_fig.add_subplot(111)  # 1面のAxes
self.ax_rgb.set_xticks([]); self.ax_rgb.set_yticks([])  # 目盛り非表示
self.rgb_canvas = FigureCanvasTkAgg(self.rgb_fig, master=self.tab_rgb)  # Tkに埋め込み
self.rgb_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=6, pady=(2, 6))  # 余白、Windowリサイズ
self.rgb_fig.canvas.mpl_connect("button_press_event", self.on_image_click)  # クリックイベント
self.nb.bind("<>", self.on_tab_changed)  # タブ切替で再描画

Gray画像表示用と同様に、疑似RGB表示用に Matplotlibの Figure(figsize=(6,6), dpi=100)を作成し、その中にadd_subplot(111) で 1 つの描画領域(Axes)を用意します。
軸目盛りを消して画像を見やすくしている点や、余白やリサイズ追随する点、画像クリックを捕捉し “+” マーカーを表示しスペクトルグラフを表示する点に関してもGray画像のと同様です。

最後に "<<NotebookTabChanged>>" をバインドし、タブ切替時にも最新のマーカーを復元するための再描画を追加しています。

(2)-5 右側:スペクトルグラフ表示

スペクトルビューアー_メニュー
①右側のコンテナ作成
def _build_right_panel(self):
    hdr = ttk.Frame(self.right); hdr.pack(fill=tk.X, padx=6, pady=(6, 0))  # 見出し行
    ttk.Label(hdr, text="Spectra", font=("", 11, "bold")).pack(side=tk.LEFT)  # タイトル
    ttk.Button(hdr, text="Reset Spectra", command=self.reset_spectra).pack(side=tk.RIGHT)  # リセットボタン

右側のコンテナに、スペクトル表示用のヘッダー行を作ります。

ttk.Frame を横一杯に配置し、左側に “Spectra” の文字、 右側に[Reset Spectra]ボタンを配置します。
[Reset Spectra]ボタンを押した際は、"reset_spectra()" が実行され、スペクトルグラフと画像上の“+”マーカーを一括クリアが実施されます。
(5章「(3)スペクトルグラフと”十”マーカーのリセット機能」で別途解説)

②スペクトルグラフ表示
self.spec_fig = Figure(figsize=(6, 6), dpi=100)  # スペクトル用Figure
self.ax_spec = self.spec_fig.add_subplot(111)  # 1面Axes
self._style_spec_axes()  # 軸の基本スタイル設定
self.spec_canvas = FigureCanvasTkAgg(self.spec_fig, master=self.right)  # Tkに埋め込み
self.spec_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=6, pady=(4, 0))  # 余白、Windowリサイズ
tb = NavigationToolbar2Tk(self.spec_canvas, self.right, pack_toolbar=False)  # ツールバー設置
tb.update(); tb.pack(side=tk.BOTTOM, fill=tk.X)  # 下部に配置

スペクトルグラフ表示エリアを構築します。

まず、スペクトル用に Matplotlib の Figure(figsize=(6, 6), dpi=100) を作成し、add_subplot(111) で 1 つの描画領域(Axes)を用意します。

続いて _style_spec_axes() を呼び、タイトル・軸ラベル・グリッドなどの基本スタイルを適用します。次に FigureCanvasTkAgg で Figure を Tk ウィジェット化し、pack(fill=BOTH, expand=True, padx=6, pady=(4, 0)) で右側に埋め込みます。
画像をクリックした際は on_image_click() が呼ばれ、この グラフにスペクトルが重ね描きされます。

最後に、スペクトルグラフに対してズーム・保存といった操作を可能にするツールバー( NavigationToolbar2Tk )を下部へ配置します。

3. データ読み込みプログラム

ここからは、作成したGUIを操作した際に実行される機能のプログラムを紹介していきます。

まずは[Open HDR…]を実行してハイパースペクトルデータを読み込むためのプログラムです。
(1)ファイル選択と読み込み、(2)配列形状の正規化、(3)波長配列の取得と初期化、(4)Gray/RGBの初期バンド決定、(5)スライダー設定とラベル更新、(6)GUIの有効化と初期再描画という手順でデータの読み込みを来ないます。

ここで使用する”Spectral Python”のスペクトルデータ読み込みコマンドの詳細については、以下を参考にしてください。
1.1ー>リンク

(1) ファイル選択と読み込み(基本エラー処理含む)

# === Data Loading ========================================================
def open_hdr(self):
    path = filedialog.askopenfilename(  # HDR選択ダイアログを開く
        title="Select HDR file", filetypes=[("ENVI header", "*.hdr"), ("All files", "*.*")]  # フィルタ設定
    )
    if not path:
        return  # キャンセル時は何もしない
    try:
        img = spectral.open_image(path)  # ENVIヘッダを開く
        data = np.asarray(img.load())  # データ本体をNumPy配列化
    except Exception as e:
        messagebox.showerror("Load error", str(e))  # 失敗時はエラーダイアログ
        return

filedialog.askopenfilename()で ユーザーがファイルエクスプローラーから.hdrファイルを選択できるようにします。
ファイルが選択されたら、spectral.open_image(path)→img.load()でスペクトルデータを読み込み、データを扱いやすくするために numpyの配列化を行います。
読み込み失敗した時にはエラーを表示して中断、成功時は 次の形状の正規化に進みます。

(2) データ形状の正規化と状態保存

if data.ndim == 2:
    data = data[:, :, None]  # 2Dのときは (H,W,1) に拡張

self.img, self.data = img, data  # selfに格納
self.path_var.set(os.path.abspath(path))  # 上部ラベルに絶対パスを表示

読み込んだスペクトルデータの波長が1つで、二次元配列(data.ndim == 2)になっている時のために、 data[:, :, None] として (H, W, 1) に拡張することで、以降の処理を3次元配列「(H, W, B) 」で統一されるようにします。
読み込んだimgdataは、self.img, self.data に保存することで、各関数で簡単に使用できるようになります。
トップバーのラベル os.path.abspath(path)self.path_var.set() で更新することで、 GUIの 上部に読み込んだファイルのパスが表示されます。

(3) 波長配列の取得と 初期表示バンドの決定

try:
    wl_meta = img.metadata.get("wavelength", None)  # 波長メタデータ取得
    wl = to_float_array(wl_meta) if wl_meta is not None else np.arange(data.shape[2], dtype=float)  # 変換または代替
except Exception:
    wl = np.arange(data.shape[2], dtype=float)  # 失敗時は0..B-1を波長扱い
self.wavelengths = wl  # 波長配列を保存
self.gray_band = min(10, data.shape[2]-1)  # Gray画像初期バンド(10番目か最終に近い)
B = data.shape[2]  # バンド数
self.rgb_bands = {"R": int(B*0.8), "G": int(B*0.5), "B": int(B*0.2)}  # 疑似RGBの初期割当

img.metadata.get("wavelength") を取り出し、存在すればfloat化した配列へ変換。欠落・解析失敗時は np.arange(B, dtype=float) を採用し「波長=バンド番号」として扱います。
結果は self.wavelengths へ保存し、以降のスライダーやラベル、スペクトル横軸に用います。

Gray 画像のバンド初期値は、ノイズの乗りやすい初期バンドを避けて、 バンド数が多い場合は10バンド目、少ない場合は、最終バンドの1バンド下とします。
偽RGB画像のバンド初期値は、それぞれバンド幅全体の (0.2/0.5/0.8) 近辺を B/G/R に割り当てます。これにより、可視光領域全体をカバーするスペクトルカメラで撮影した画像の場合には、見た目に近くなります。

(4) スライダー範囲と現在値の反映

wl_min, wl_max, res = float(wl.min()), float(wl.max()), self._wl_resolution()  # スライダー範囲と分解能
self.gray_scale.configure(from_=wl_min, to=wl_max, resolution=res)  # Grayスライダー設定
self.gray_scale_var.set(float(wl[self.gray_band]))  # 現在のスライダー値に対応する波長を反映

for sc in (self.r_scale, self.g_scale, self.b_scale):
    sc.configure(from_=wl_min, to=wl_max, resolution=res)  # R/G/B各スライダーの範囲設定
self.r_var.set(float(wl[self.rgb_bands["R"]]))  # R初期波長
self.g_var.set(float(wl[self.rgb_bands["G"]]))  # G初期波長
self.b_var.set(float(wl[self.rgb_bands["B"]]))  # B初期波長
self._update_rgb_labels()  # R/G/Bのラベルを更新

まずは、HDR読込後に取得したwlから抽出したmin,maxでスライダーの範囲と分解脳を決定し、Gray スライダーに設定します。現在値には wl[self.gray_band] を適用します。
同様に、R/G/B の各スライダーにも範囲を設定し、それぞれの波長を割り当てます。
さらに _update_rgb_labels() で「Band / λ」表示を揃え、GUI と設定値を同期させます。

(5) GUI有効化と表示の初期化

self._set_sliders_state(tk.NORMAL)  # スライダーを有効化
self._update_gray_label()  # Gray側のラベル更新
self.reset_spectra()  # スペクトル&マーカー初期化→両画像再描画

スライダー関連の設定が完了したので、読み込み前に無効化していたスライダー群を有効化します。
HDR読み込みの最後の処理として、Gray 表示の表示ラベルの更新し、self.reset_spectra() で画像上の “+” マーカーと右側のスペクトルグラフの初期化、Gray/RGB 画像を再描画を実施します。

(6) 波長配列のfloat 化関数

def to_float_array(x): 
    try:
        return np.array(x, dtype=float)  # そのままfloat配列化を試行
    except Exception:
        return np.array([float(str(v).strip().replace(",", "")) for v in x])  # カンマ除去などのフォールバック

波長配列を例外処理を行ったうえで、float化したnumpy配列にする関数です。
ENVI メタデータの wavelength は文字列リストやカンマ付き表記を含む場合があります。
まずは、配列をそのままnp.array(..., dtype=float)でfloat化し、失敗時には、文字列の空白やカンマを除去した後に処理を行います。
これにより、スライダーや横軸で確実に利用できるようになります。

4. スペクトル画像表示プログラム

スライダー操作やタブ切り替えが行われたときに、選択波長をラベルに反映し、単バンド/偽RGB画像を描画します。
このときに“+”マーカーを復元する必要がある点に注意が必要です。
再読込時にはGUIと内部状態を同期し、画像は背面、マーカーは前面に重ねて一貫性を保ちます。

(1) 波長ラベル更新関数

①Gray画像用
def _update_gray_label(self):
    b = int(self.gray_band)  # 現在のバンド番号
    wl = float(self.wavelengths[b]) if self.wavelengths is not None else float(b)  # 波長値(なければ番号)
    self.gray_sel_var.set(f"Band {b} (lambda={wl:.3f})")  # 表示文字列を更新

選択中のGray画像のバンド番号を整数に整え、波長配列があれば該当バンドの実波長、無い場合はバンド番号をそのまま表示に使用します。取得した波長は、self.gray_sel_var にセットして即時にラベルへ反映します。
この関数は、スライダー操作のたびに呼ばれ、GUI上の波長ラベルの状態表示を常に最新に保ちます。

②偽RGB画像用
def _update_rgb_labels(self):
    def t(k):
        b = self.rgb_bands[k]  # 指定キーのバンド番号
        return "-" if b is None else f"Band {b} (lambda={float(self.wavelengths[b]):.3f})"  # 表示用
    self.r_label.set(f"R: {t('R')}"); self.g_label.set(f"G: {t('G')}"); self.b_label.set(f"B: {t('B')}")  # 3本分更新

R/G/B 各キーに対応するバンド番号を取り出し、バンドから波長を読み出します。バンド番号と波長を「Band i (lambda=…)」の形式にして、RGBの各スライダーに表示します。

(2) 画像の再描画関数(マーカー復元付き)

①Gray画像用
def _update_gray_image(self):
    b = int(np.clip(self.gray_band, 0, self.data.shape[2]-1))  # バンド番号を範囲内に
    ax = self.ax_gray  # 対象Axes
    ax.cla(); ax.set_title(f"Gray — Band {b}"); ax.set_xticks([]); ax.set_yticks([])  # クリア&装飾
    ax.imshow(self.data[:, :, b], cmap=self.cmap_name.get(), zorder=0)  # 画像を背面で表示
    self._draw_markers(ax)  # 保存済み“+”を前面に再描画
    self.gray_canvas.draw()  # キャンバス更新

選択バンド番号を画像が存在する範囲のバンドにクリップし、Gray画像用の Matplotlib の Axes オブジェクト(self.ax_gray)を、ローカル変数 ax に格納します。
その後、self.ax_grayの初期化、タイトルと軸目盛り非表示化をした上で、最後に、imshow で単バンド画像を選択中のカラーマップで描画します。
さらに、画像の上位レイヤーに保存済みの “+” マーカーを描画し、self.gray_canvas.draw() によりキャンバスへ適用します。

②偽RGB画像用
def _update_rgb_image(self):
    R, G, Bn = self.rgb_bands["R"], self.rgb_bands["G"], self.rgb_bands["B"]  # 選択バンド
    ax = self.ax_rgb  # 対象Axes
    ax.cla(); ax.set_xticks([]); ax.set_yticks([])  # クリア&装飾
    if None in (R, G, Bn):
       ax.set_title("Pseudo RGB (select R/G/B)")  # 未選択時の案内
    else:
        R = int(np.clip(R, 0, self.data.shape[2]-1))  # 範囲内に丸める
        G = int(np.clip(G, 0, self.data.shape[2]-1))
        Bn = int(np.clip(Bn, 0, self.data.shape[2]-1))
        rgb = np.dstack([
            stretch01(self.data[:, :, R]),  # Rを[0,1]へ伸長 
            stretch01(self.data[:, :, G]),  # Gを[0,1]へ伸長
            stretch01(self.data[:, :, Bn])  # Bを[0,1]へ伸長
        ])
        ax.set_title(f"Pseudo RGB — R:{R} G:{G} B:{Bn}")  # タイトル更新
        ax.imshow(rgb, zorder=0)  # 合成画像を背面で表示
    self._draw_markers(ax)  # “+”を前面に再描画
    self.rgb_canvas.draw()  # キャンバス更新

R/G/B の選択状態を取得し、いずれか未設定なら描画を保留します。
Gray画像と同様に、各バンドを画像が存在する範囲のバンドにクリップし、stretch01 で 0–1 正規化した後に、np.dstack でRGBを結合し偽RGB画像を作成します。
最後に、imshow で作成した偽RGB画像の描画と、 “+” マーカーを再描画を行います。

(3) 画像の0–1 正規化関数

def stretch01(img, low=2, high=98):  
    v1, v2 = np.nanpercentile(img, [low, high])  # 2–98%パーセンタイル取得
    if v2 - v1 == 0:
        return np.zeros_like(img, dtype=float)  # 全域同値ならゼロ配列
    return np.clip((img - v1) / (v2 - v1), 0, 1)  # [0,1]に正規化

ダイナミックレンジの極端なデータでも見やすい偽RGB画像にできるよう、2–98 パーセンタイル値を求め、0–1 へ正規化する関数です。
全画素が同値でレンジが 0 の場合はゼロ配列を返すという処理を追加することで、例外にも強くなります。

(4) スライダーによる波長変更機能

①Gray画像用
def on_gray_scale(self, val):
    if self.data is None or self._snapping:
        return  # 未読込/スナップ中は無視
    self._snapping = True  # 多重発火防止フラグON
    try:
        idx = self._nearest_band(float(val))  # 波長に最も近いバンドを探索
        self.gray_band = idx  # 現在バンドを更新
        self.gray_scale_var.set(float(self.wavelengths[idx]))  # スライダー値を実波長にスナップ
        self._update_gray_label()  # ラベル更新
        self._update_gray_image()  # 画像再描画(マーカー復元)
     finally:
        self._snapping = False  # 多重発火防止フラグOFF

Gray画像の波長スライダーを操作した際に実行される関数です。
データが読み込まれていない場合や、すでに関数を実行している際にスライダーを動かしたときにイベントが多重発火しないように処理中はフラグをONにします。
処理を行うべき時は、与えられた波長値から最近傍のバンドを抽出し、self.gray_band を更新します。スライダーの表示値は実波長へスナップさせ、選択ラベルを更新後、単バンド画像を再描画します。

②偽RGB画像用
def on_rgb_scale(self, _=None):
    if self.data is None or self._snapping:
        return  # 未読込/スナップ中は無視
    self._snapping = True  # 多重発火防止フラグON
    try:
        self.rgb_bands["R"] = self._nearest_band(float(self.r_var.get()))  # Rを最近傍に
        self.rgb_bands["G"] = self._nearest_band(float(self.g_var.get()))  # Gを最近傍に
        self.rgb_bands["B"] = self._nearest_band(float(self.b_var.get()))  # Bを最近傍に
        # snap sliders to exact wavelengths
        self.r_var.set(float(self.wavelengths[self.rgb_bands["R"]]))  # スライダー値を実波長へ
        self.g_var.set(float(self.wavelengths[self.rgb_bands["G"]]))
        self.b_var.set(float(self.wavelengths[self.rgb_bands["B"]]))
        self._update_rgb_labels()  # ラベル更新
        self._update_rgb_image()  # 画像再描画(マーカー復元)
    finally:
        self._snapping = False  # 多重発火防止フラグOFF

R/G/B の3 本の波長スライダーのいずれかが動いた際に実行される関数です。
基本的な構成はGray画像のスライダーを移動した際の関数と同じ構成で、多重発火防止、3つの波長をそれぞれ最近近傍バンドへスナップ、ラベルの更新、偽RGB画像の再描画を行います。

(5) スライダー関連補助関数

① スライダー有効/無効の一括切替
def _set_sliders_state(self, state):
    self.gray_scale.configure(state=state)  #Grayスライダーの有効/無効
    for sc in (self.r_scale, self.g_scale, self.b_scale):
        sc.configure(state=state)  # R/G/Bスライダーの有効/無効

スライダー を意図しないタイミングで操作されないように制御する関数です。
ファイルを読み込んでいない時の誤動作防止や、読み込み直後の初期化中に操作が行われないよう、Gray と R/G/B の各 Scale ウィジェットのState を切り替えます。

② 最近傍バンドの探索
def _nearest_band(self, wl_value):
    dif = np.abs(self.wavelengths - wl_value)  # 各波長との差の絶対値
    return int(np.argmin(dif))  # 最小差のインデックス(最近傍バンド)

与えられた波長値と self.wavelengths の絶対差を取り、最も近いバンドを返す関数です。
スライダーは実数の波長値を扱いますが、実データは離散バンドなので、この関数で“最寄りバンド”にスナップさせ、表示の一貫性と直感的な操作感を実現します。

③ スライダー刻み(波長分解能)の推定
def _wl_resolution(self):
    wl = self.wavelengths  # 波長配列
    if wl is None or wl.size < 2:
        return 1.0  # データ不足時は1.0
    diffs = np.diff(wl.astype(float))  # 隣接差
    diffs = diffs[np.isfinite(diffs)]  # 有限値のみ
    res = float(np.median(np.abs(diffs))) if diffs.size else 1.0  # 中央値を分解能に設定
    return res if np.isfinite(res) and res > 0 else 1.0  # 妥当性チェック

波長スライダーの刻み幅(resolution)を自動推定する関数です。
HDRファイルから取得した波長配列 self.wavelengths の「隣り合う値の差の代表値=実質的なバンド間隔」を求め、Scale(resolution=…) に渡します。
波長数が2以下の場合には、1.0 へ置き換えるなどの処理を加えることで安定化させています。

(7) タブ切替関数

def on_tab_changed(self, event):
    if self.data is None:
        return  # データ未読込なら無視
    nb = event.widget  # Notebook本体
    tab = nb.nametowidget(nb.select())  # 現在選択タブのウィジェット
    if tab is self.tab_gray:
        self._update_gray_image()  # Gray画像を再描画(マーカー含む)
    elif tab is self.tab_rgb:
        self._update_rgb_image()  # 偽RGB画像を再描画(マーカー含む)

タブ切替時に実行される関数で、画像表示を最新化します。
データが読み込まれていない場合は何もしません。
データが読み込まれている場合は、現在選択中のタブを取得し、Gray タブなら _update_gray_image()、Pseudo RGB タブなら _update_rgb_image() を実行します。
これにより、各更新関数により画像が再描画され、保存済みの “+” マーカーが復元されます。

スペクトルデータの画像表示に関してはこちら
https://www.klv.co.jp/corner/python-spectral-figure.html

(8) “+”マーカーの再描画(前面固定)

def _draw_markers(self, ax):
    for (x, y, color) in self.points:
        ax.plot(x, y, marker='+', color=color,
                markersize=10, markeredgewidth=1.8,
                zorder=3, clip_on=True)  # 常に前面に表示

画像を切り替えたり再描画した際に、クリック位置の”+”マークを復元するための関数です。
保持している self.points(x, y, color)の全点を、対象の画像上に marker='+' で描画します。zorder=3 で常に画像の前面に重ね、clip_on=True で座標系外の描画を抑制しています。

5. スペクトルグラフ表示プログラム

本章では、右側のスペクトルグラフ領域のしくみを解説していきます。
まずは、基本となる、軸タイトルやグリッドなど見やすさの設定を行い、画像クリックに応じてスペクトルを追加する処理を追加します。
さらに、リセットボタンでグラフと“+”マーカーを一括クリアする、”+”マーカーとスペクトルの色の割り当てるなどの仕組みも構築していきます。

(1) スペクトルグラフのスタイル設定

def _style_spec_axes(self):
    self.ax_spec.set_title("Spectra")  # 右側グラフのタイトル
    self.ax_spec.set_xlabel("Wavelength")  # x軸ラベル(波長)
    self.ax_spec.set_ylabel("Reflectance")  # y軸ラベル(反射率)
    self.ax_spec.grid(True)  # グリッド表示ON

右側のスペクトルグラフ用の Axes の、グラフタイトル、x軸ラベル、y軸ラベル、グリッド表示の有効化を行います。

(2) 画像クリックによるスペクトルグラフ描画機能

def on_image_click(self, event):
    if self.data is None: return  # データ未読込なら無視
    if event.inaxes not in (self.ax_gray, self.ax_rgb): return  # 対象Axes外は無視
    if event.xdata is None or event.ydata is None: return  # 座標未取得は無視

    x, y = int(round(event.xdata)), int(round(event.ydata))  # クリック座標(整数化)
    H, W, _ = self.data.shape  # 画像サイズ
    if not (0 <= x < W and 0 <= y < H): return  # 範囲外なら無視

    color = self._marker_color(len(self.points))  # 色の取得
    self.points.append((x, y, color))  # 履歴に追加(後の再描画で使用)

    spectrum = np.asarray(self.data[y, x, :]).squeeze()  # その画素のスペクトル
    self.ax_spec.plot(self.wavelengths, spectrum, marker='o', lw=1.5,
                      color=color, label=f'({x},{y})')  # スペクトルを重ね描き
    self.ax_spec.legend(loc='best'); self.spec_canvas.draw()  # 凡例更新&描画

    event.inaxes.plot(x, y, marker='+', color=color,
                      markersize=10, markeredgewidth=1.8,
                      zorder=3, clip_on=True)  # 画像上に即時“+”表示(前面)
    (self.gray_canvas if event.inaxes is self.ax_gray else self.rgb_canvas).draw()  # 対応キャンバス更新

画像(Gray/偽RGBのどちらでも)をクリックしたときに実行される関数です。
まずは、クリックが無効領域や座標未取得の場合は無視。クリックが画像内であれば座標を整数化します。
次に、前準備として、グラフや”+”マークの追加にあたって、新規色の割り当て、self.points への座標を保存を行います。

最後に、スペクトルグラフにその画素のスペクトルを追加(凡例に座標を表記)し、クリック位置への “+”マークを描画します。このとき、”+”マークが必ず画像より上のレイヤーに表示されるように zorder=3 としています。

スペクトルグラフ表示
https://www.klv.co.jp/corner/spectral-python-graph.html

(3) スペクトルグラフと"十"マーカーのリセット機能

def reset_spectra(self):
    self.ax_spec.cla()  # スペクトルAxesをクリア
    self._style_spec_axes()  # 軸スタイルを復元
    self.spec_canvas.draw()  # 右スペクトルグラフ更新
    self.points.clear()  # “+”履歴を空にする
    self._update_gray_image()  # 左Gray画像再描画
    self._update_rgb_image()  # 左偽RGB画像再描画

“クリア”ボタンが押された際に、スペクトルグラフと、画像上の”+”マークを消すための関数です。
右側のスペクトルグラフを cla() でクリアし、共通スタイルを _style_spec_axes() で復元してからキャンバスを再描画します。さらに、画像上の “+” マーカーの履歴である self.points をクリアし、左の Gray/偽RGB 画像をそ再描画することで、マーカーを消します。

(4) マーカー色の決定(循環パレット)

def _marker_color(self, i):
    return plt.cm.tab10(i % 10)  # 10色の循環カラーマップから選択

画像の”十”マーク、スペクトルグラフで一貫した色使いを行うために、plt.cm.tab10 の 10 色を i % 10 で循環させ、クリック順に色を割り当てる関数です。
色数を増やしたい場合は別のカラーマップに差し替えるだけで拡張することができます。

6. 実行エントリーポイントの設置

# === Entry Point =========================================================
if __name__ == "__main__":  # スクリプトとして実行された場合
    HyperspecTk().mainloop()  # アプリを生成してTkのイベントループ開始

プログラムが直接実行されたときにHyperspecTk() でGUI(Tkウィンドウ)を生成し、mainloop() を呼んでイベントループ(クリックやキー入力、再描画などの処理待ち)を開始するための記述です。
これによりGUIが表示・操作可能な状態になります。

if __name__ == "__main__": は「このプログラムが直接実行されたとき」にという慣用句で、他のモジュールからimportされた場合は実行されません。

7. まとめ

今回作成したビューアは、①上部の HDR 読み込み、②左側の Gray/偽 RGB 表示(波長スライダー・カラーマップ選択)、③右側のスペクトルプロット(画像クリックでグラフ追加、マーカー保持、リセット)という構成で、解析の入口に必要な操作を一画面に集約しました。

波長スライダーは実波長へスナップし、擬似RGBはパーセンタイル伸長で見栄えを調整、クリック点は色付き“+”で追跡できます。
これをベースとして、必要に応じて統計解析を用いた、分類・定量化の機能を拡張していくことができます。

ハイパースペクトルカメラコース

ご質問・ご相談お気軽にお問い合せください

お電話でのお問合せ 03-3258-1238 受付時間 平日9:00-18:00(土日祝日除く)
Webでのお問い合わせ