サーモグラフィAMG8833のデータをPythonでリアルタイムに可視化

# はじめに
この記事は，8x8の赤外線アレイセンサ「AMG8833」をArduinoから利用して，そのデータをリアルタイムにPCに送信しPython（PyQt5，PyQtGraph）で可視化したときの記録です．

下の画像のような感じでリアルタイムに可視化しています．![amg8833.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/a70d1fb4-d0bf-bfa4-4f4a-a8c8bcdc85b8.gif)

# Arduino
Arduinoはサーモグラフィのデータを取得し，PC（Python）にシリアル通信で送信します．
## AMG8833
今回はスイッチサイエンスの[Conta™ サーモグラフィー AMG8833搭載](https://www.switch-science.com/catalog/3395/)を使用しています．

スイッチサイエンスによると
>各素子ごとの温度測定範囲は0℃～80℃です。測定エリアはセンサ正面（上下左右約60度）の四角錘で、このエリアを8x8ピクセルに分割した2次元画像が得られます。

またAMG8833を簡単に使えるように[Conta™ ベースシールド](https://www.switch-science.com/catalog/2769/)を利用しています．

## データ取得＆送信
スイッチサイエンスのページにはAMG8833のライブラリがいくつか挙げられていますが，ここではSparkFunの[SparkFun_GridEYE_Arduino_Library](https://github.com/sparkfun/SparkFun_GridEYE_Arduino_Library)を利用しています．以下のプログラムは[サンプルプログラム](https://github.com/sparkfun/SparkFun_GridEYE_Arduino_Library/blob/master/examples/Example1-SerialVisualizer/Example1-SerialVisualizer.ino)を少し変えただけです．

シリアル通信では，8x8=64個の温度データを「,」区切りの文字列として送信しています．

```c:amg8833-serial.ino
#include <SparkFun_GridEYE_Arduino_Library.h>
#include <Wire.h>

float pixelTable[64];

GridEYE grideye;

void setup() {
  Wire.begin();
  // スイッチサイエンスのAMG8833のアドレスはデフォルトでは0x68に設定されている．
  grideye.begin(0x68);
  Serial.begin(115200);
}

void loop() {
  // 8x8=64個のピクセルに対して温度を取得しgrideyeに保存する．
  for(unsigned char i = 0; i < 64; i++){
    pixelTable[i] = grideye.getPixelTemperature(i);
  }
  // すべてのピクセルの温度を","で区切り1行に並べてシリアル通信で送信する．
  for(unsigned char i = 0; i < 64; i++){
    Serial.print(pixelTable[i]);
    if(i != 63){
      Serial.print(",");
    }
  }
  Serial.println();
  delay(50);
}
```

# Python
Arduinoから受け取った温度データをもとに，ヒートマップをリアルタイムに表示します．

## ライブラリ
- pyserial - シリアル通信
- numpy
- pyqt5
- pyqtgraph - リアルタイム描画
- (matplotlib) - カラーマップ

Pythonでデータ可視化といえばmatplotlibですが，matplotlibでリアルタイムに画面を更新するには重すぎるという欠点があります．そのためmatplotlibではなく，軽量な[pyqtgraph](http://www.pyqtgraph.org/)というライブラリを利用しています．

といいつつ今回は，温度データを色（RGBA）に変換するためにmatpotlibの一部を使っています．pyqtgraphでも近いことはできるようですが，その場合は自分でカラーマップに使う色を指定する必要があるようです．ここではデフォルトでカラーマップを用意してくれているmatplotlibを使います．

## データ受信＆ヒートマップ表示

```python:heatmap.py
from pyqtgraph.Qt import QtGui, QtCore
import matplotlib.cm as cm
import matplotlib as mpl
import pyqtgraph as pg
import numpy as np
import serial

# ヒートマップを見やすくするためにTEMP_MAX=40.0に設定
TEMP_MIN = 0.0
TEMP_MAX = 40.0

app = QtGui.QApplication([])

win = pg.GraphicsLayoutWidget(show=True, title="AMG8833")
win.resize(600,600)
win.setWindowTitle('AMG8833')
pg.setConfigOptions(antialias=True)

view = win.addViewBox()
view.setAspectLocked(True)

img = pg.ImageItem(border='w')
view.addItem(img)

# 温度を色に変換するためのColor Map
norm = mpl.colors.Normalize(vmin=TEMP_MIN, vmax=TEMP_MAX)
cmap = cm.jet
m = cm.ScalarMappable(norm=norm, cmap=cmap)

# シリアル通信
ser = serial.Serial("COM13", 115200, timeout=1)

# 8x8の温度データ
data = np.zeros((8, 8))

def get_data():
    global data, ser
    if ser.in_waiting:
        # Arduinoからデータ受信
        # データ取得時には何かとエラーが出るので簡単に例外処理
        try:
            line = ser.readline().decode().strip()
            temps = [float(t) for t in line.split(",")]
            data = np.array(temps).reshape((8, 8))
        except:
            pass

def update_plot():
    global img, data
    img.setImage(m.to_rgba(data))

# 50msごとにデータ受信
timer1 = QtCore.QTimer()
timer1.timeout.connect(get_data)
timer1.setInterval(50)
timer1.start()

# 100msごとにヒートマップ更新  
timer2 = QtCore.QTimer()
timer2.timeout.connect(update_plot)
timer2.setInterval(100)
timer2.start()

if __name__ == '__main__':
    import sys
    if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
        QtGui.QApplication.instance().exec_()
```

上のプログラムではデータ受信とヒートマップ更新を別のQTimerに分けています．データを受信するたびにヒートマップを更新していると，ヒートマップの更新が間に合わなくなることがあります．それを防ぐために別々にデータ受信とヒートマップ更新を個別にできるようにしています．

# 参考リンク

- [AMG8833のデータを簡易サーモグラフィぽくブラウザで可視化(前編)](https://masato-ka.hatenablog.com/entry/2018/01/05/172212)