はじめに
リアルタイムでヒートマップを作成する機会があり、matplotlibでは処理速度が不安だった為pyqtgraphを使用しました。しかしpyqtgraphは日本語の情報が少なく、ヒートマップに使うカラーバーもデフォルトでは無かった為色々調べた結果を記載します。
環境
Mac OS
Python 3.8.1
pipでインストールしたもの
colour 0.1.5
numpy 1.19.2
pgcolorbar 1.0.0
PyQt5 5.15.2
PyQt5-sip 12.8.1
pyqtgraph 0.11.0
pip install colour numpy pgcolorbar PyQt5 PyQt5-sip pyqtgraph
pgcolorbar
pyqtgraphでカラーバーを作成してくれるライブラリです。
pipでインストールした後下記コマンドでdemoが見れます。
pgcolorbar_demo
詳細
リアルタイム描画は今回見送りしてヒートマップのみの説明をします。
最終的に以下の画面を作成します。
(2021/01/19 修正前)
サンプルデータ
説明に使用するサンプルデータです。色々やってますが特に意味はありません。
2次元配列の為RGB情報はありません。
# サンプル配列
data = np.random.normal(size=(200, 200))
data[40:80, 40:120] += 4
data = pg.gaussianFilter(data, (15, 15))
data += np.random.normal(size=(200, 200)) * 0.1
画像表示の最小コード(とする)
以降は下記コードに追記してヒートマップを作成していきます。
行っていることは
- ウィンドウ作成
- 画像作成
- 画像をビューに追加
- グラフを作成(画像の座標表示の為)
- グラフに3.で作成したビューを追加
- ウィンドウにグラフを追加
- 表示
です
"""pyqtgraphで画像を表示する最小コード(とする)"""
import sys
import numpy as np
from PyQt5 import QtWidgets
import pyqtgraph as pg
# サンプルデータのコードは省略
# GUIの制御オブジェクト
app = QtWidgets.QApplication(sys.argv)
# ウィンドウ作成
window = pg.GraphicsLayoutWidget()
# 画像オブジェクト作成 & 画像をセット
image = pg.ImageItem()
image.setImage(data)
# 画像を格納するボックス作成 & 画像オブジェクトをセット
view_box = pg.ViewBox()
view_box.addItem(image)
# プロットオブジェクト作成 & 上で作成したview_boxをセット
plot = pg.PlotItem(viewBox=view_box)
# ウィンドウにplotを追加
window.addItem(plot)
# ウィンドウ表示
window.show()
# プログラム終了
sys.exit(app.exec_())
この状態で実行すると以下の画面が作成されます。
ViewBox
以下を追加します。
view_box.setAspectLocked(lock=True)
view_boxのアスペクト比を固定します。画像サイズが200×200の正方形の為、ウィンドウサイズが変わった時に形が変わってほしくないからです。
追加した周辺のコード
view_box = pg.ViewBox()
view_box.setAspectLocked(lock=True) # Add
view_box.addItem(image)
PlotItem
編集する個所はありません。
ImageItem
追記した箇所とその周辺を下記に記載します。
################## Add #######################
from colour import Color
blue, red = Color('blue'), Color('red')
colors = blue.range_to(red, 256)
colors_array = np.array([np.array(color.get_rgb()) * 255 for color in colors])
look_up_table = colors_array.astype(np.uint8)
################## Add #######################
image = pg.ImageItem()
image.setLookupTable(look_up_table) # Add
image.setImage(data)
from colour import Color
中身の詳細を記載します。この項で作成したいものはRGB情報の2次元配列(符号なし8bit整数)です
[[R, G, B],
[R, G, B],
...
[R, G, B],]
blue, red = Color('blue'), Color('red')
# >>> <class 'colour.Color'> <class 'colour.Color'>
blue.get_rgb()
# >>> (0.0, 0.0, 1.0)
colors = blue.range_to(red, 256) # 青から赤までの色を256分割
# >>> generator リストにして中身を見ると
# >>> [<Color blue>, <Color #0004ff>, <Color #0008ff>, ..., <Color red>]
colors_array = np.array([np.array(color.get_rgb()) * 255 for color in colors])
# >>> [[ 0. 0. 255.]
# [ 0. 4. 255.]
# [ 0. 8. 255.]
# ...
# [255. 0. 0.]] float64
look_up_table = colors_array.astype(np.uint8) # 符号なし8bit整数に変換
# >>> [[ 0 0 255]
# [ 0 3 255]
# ...
# [255 0 0]] uint8
Look Up Table
入力色データに対応する出力色データをLook Up(参照)するTable(対応表)
第一回 LUTの基本 | TVLogic
らしいです。(あまり詳しくなくてすみません、、、)
サンプルデータの値をRGB情報に変換してくれる表のようなイメージです。
2021/01/19 追記
image.setOpts(axisOrder='row-major')
上記コードを追加して下さい。
pg.ImageItem
はデフォルトで列 -> 行で2次元配列を読み込みます。
2次元配列は基本的に行列なので行 -> 列で読み込む為に必要です。
書かなかった場合、元データの転置行列のように読み込まれます。
修正後のコード
################## Add #######################
from colour import Color
blue, red = Color('blue'), Color('red')
colors = blue.range_to(red, 256)
colors_array = np.array([np.array(color.get_rgb()) * 255 for color in colors])
look_up_table = colors_array.astype(np.uint8)
################## Add #######################
image = pg.ImageItem()
image.setOpts(axisOrder='row-major') # 2021/01/19 Add
image.setLookupTable(look_up_table) # Add
image.setImage(data)
ColorLegendItem
カラーバーを作成します
以下を追加します。
################## Add #######################
from pgcolorbar.colorlegend import ColorLegendItem
color_bar = ColorLegendItem(imageItem=image, showHistogram=True)
color_bar.resetColorLevels()
window.addItem(color_bar)
################## Add #######################
imgeItem
にはpg.imgItem
を渡します。showHistogram
はカラーバー横のヒストグラムを表示するかを決められます。デフォルトはTrueです。
resetColorLevels()
でカラーバーの範囲をデータの最大、最小値に設定します。
2021/01/20 追記
label
引数でカラーバーのラベルを設定できます。
color_bar = ColorLegendItem(imageItem=self.image, showHistogram=True, label='sample')
全体
全体のコード
"""全体コード"""
import sys
from colour import Color
import numpy as np
from pgcolorbar.colorlegend import ColorLegendItem
from PyQt5 import QtWidgets
import pyqtgraph as pg
# サンプル配列
data = np.random.normal(size=(200, 200))
data[40:80, 40:120] += 4
data = pg.gaussianFilter(data, (15, 15))
data += np.random.normal(size=(200, 200)) * 0.1
app = QtWidgets.QApplication(sys.argv)
window = pg.GraphicsLayoutWidget()
blue, red = Color('blue'), Color('red')
colors = blue.range_to(red, 256)
colors_array = np.array([np.array(color.get_rgb()) * 255 for color in colors])
look_up_table = colors_array.astype(np.uint8)
image = pg.ImageItem()
image.setOpts(axisOrder='row-major') # 2021/01/19 Add
image.setLookupTable(look_up_table)
image.setImage(data)
view_box = pg.ViewBox()
view_box.setAspectLocked(lock=True)
view_box.addItem(image)
plot = pg.PlotItem(viewBox=view_box)
color_bar = ColorLegendItem(imageItem=image, showHistogram=True, label='sample') # 2021/01/20 add label
color_bar.resetColorLevels()
window.addItem(plot)
window.addItem(color_bar)
window.show()
sys.exit(app.exec_())
2021/02/04 追記
色々触ってみて最終的にこんな感じになったので記載します
2021/02/05
関数名を少し変えた & def current_image
でNoneが返ってくることがあるのでその表記を追加
-
set_first_image
->set_image
-
set_image
->update_image
-
disable_move_bar
->set_mouse_enabled_bar
(TrueとFalseの関係が逆になってた) -
reset_range_bar
->reset_color_levels
from typing import Optional
from colour import Color
import numpy as np
from PyQt5 import QtWidgets
import pyqtgraph as pg
from pgcolorbar.colorlegend import ColorLegendItem
class HeatMapWidget(QtWidgets.QGraphicsWidget):
"""ヒートマップウィジット
Attributes
----------
image: pg.ImageItem
ヒートマップで表示する画像
view_box: pg.ViewBox
画像表示用ボックス
plot: pg.PlotItem
imageの画像サイズを表示するプロット
color_bar: ColorLegendItem
カラーバー
layout: QtWidgets.QGraphicsGridLayout
plotとcolor_barを設置するレイアウト
"""
def __init__(self, title: str = '', bar_label: str = '') -> None:
"""コンストラクタ
Parameters
----------
title: str default=''
グラフタイトル
bar_label: str default=''
カラーバーラベル
"""
super(HeatMapWidget, self).__init__()
self.image = pg.ImageItem(axisOrder='row-major')
self.image.setLookupTable(self.make_lookup_table())
self.view_box = pg.ViewBox(lockAspect=True)
self.view_box.addItem(self.image)
self.plot = pg.PlotItem(viewBox=self.view_box)
self.plot.setTitle(title)
self.plot.setLabels(bottom='X', left='Y')
self.color_bar = ColorLegendItem(imageItem=self.image,
label=bar_label)
self.layout = QtWidgets.QGraphicsGridLayout()
self.layout.setContentsMargins(1, 1, 1, 1)
self.layout.setSpacing(0)
self.layout.addItem(self.plot, 0, 0)
self.layout.addItem(self.color_bar, 0, 1)
self.setLayout(self.layout)
# 画像サイズ最適化ボタン(左下の小さい奴)が押された時, ついでにカラーバーの範囲も最適化する
self.plot.autoBtn.clicked.connect(self.color_bar.resetColorLevels)
@property
def current_image(self) -> Optional[np.ndarray]:
"""表示中の画像配列を返す
画像が入っていない場合Noneを返す
Returns
----------
image.image: np.ndarray
表示画像
"""
return self.image.image
def set_image(self, image: np.ndarray) -> None:
"""画像を挿入する. 1回目の挿入を想定している為,
画像サイズの最適化 & カラーバーの範囲を画像データに合わせる
Parameters
----------
image: np.ndarray
表示したい画像配列
"""
self.image.setImage(image)
self.plot.autoBtnClicked()
self.color_bar.resetColorLevels()
def update_image(self, image: np.ndarray) -> None:
"""画像を更新する.
Parameters
----------
image: np.ndarray
表示したい画像配列
"""
self.image.updateImage(image)
def set_lower_level(self, lower: float) -> None:
"""カラーバーの最小値を設定する
Parameters
----------
lower: float
設定したい最小値
"""
self._set_levels(lower, None)
def set_upper_level(self, upper: float) -> None:
"""カラーバーの最大値を設定する
Parameters
----------
upper: float
設定したい最大値
"""
self._set_levels(None, upper)
def _set_levels(self, new_lower: Optional[float],
new_upper: Optional[float]) -> None:
""" カラーバーの範囲を設定する.
pgcolorbar_demoのプログラムを参考
- 新しい下限値が今の上限値より大きい時(new_lower > old_upper)
新しい下限値はそのままにして, 上限値をnew下限 +1する
- 新しい上限値が今の下限値より小さい時(new_upper < old_lower)
新しい上限値はそのままにして, 下限値をnew上限 -1する
Parameters
----------
new_lower: Optional[float]
設定したいカラーバーの最小値
new_upper: Optional[float]
設定したいカラーバーの最大値
"""
old_lower, old_upper = self.color_bar.getLevels()
if new_lower is None:
new_lower = old_lower
if new_upper <= new_lower:
new_lower = new_upper - 1
if new_upper is None:
new_upper = old_upper
if new_upper <= new_lower:
new_upper = new_lower + 1
self.color_bar.setLevels((new_lower, new_upper))
def reset_color_levels(self) -> None:
"""カラーバーの表示値範囲を画像データと合わせる.
カラーバーの操作は有効に戻す.
See Also
----------
set_mouse_enabled_bar
"""
self.set_mouse_enabled_bar(True)
self.color_bar.resetColorLevels()
def set_mouse_enabled_bar(self, flag: bool) -> None:
"""マウスからのカラーバー操作を有効/無効にする.
Parameters
----------
flag: bool
有効/無効を決めるフラグ
Trueの時有効にする
"""
self.color_bar.overlayViewBox.setMouseEnabled(y=flag)
self.color_bar.histViewBox.setMouseEnabled(y=flag)
@staticmethod
def make_lookup_table(lower_c: str = 'blue',
upper_c: str = 'red') -> np.ndarray:
"""ルックアップテーブル作成
Parameters
----------
lower_c: str default='blue'
カラーバー下限値の色, 16進数表記も可能 ex) '#0000ff'
upper_c: str default='red'
カラーバー上限値の色, 16進数表記も可能 ex) '#ff0000'
Returns
----------
lookup_table: np.ndarray
カラーバーの色を指定するテーブル
"""
lower_color, upper_color = Color(lower_c), Color(upper_c)
colors = lower_color.range_to(upper_color, 256)
table = np.array([color.get_rgb() for color in colors]) * 255
lookup_table = table.astype(np.uint8)
return lookup_table
追記使用例
1枚だけ画像を入れています
if __name__ == '__main__':
import sys
import numpy as np
image1 = np.random.randint(0, 99, (50, 50))
app = QtWidgets.QApplication(sys.argv)
win = pg.GraphicsLayoutWidget()
hm1 = HeatMapWidget() # これ
hm2 = HeatMapWidget() # これ
hm3 = HeatMapWidget() # これ
hm4 = HeatMapWidget() # これ
hm1.set_image(image1) # hm1だけ画像追加
# 追加して使う
win.addItem(hm1)
win.addItem(hm2)
win.nextRow()
win.addItem(hm3)
win.addItem(hm4)
win.show()
sys.exit(app.exec_())
参考
GitHub - titusjan/pgcolorbar: Color bar to use in PyQtGraph plots