[C#/WindowsIoT] RaspberryPi3にI2Cサーマルカメラ(サーモグラフィ)をつなげて温度を画像化する

**もくじ**
→https://qiita.com/tera1707/items/4fda73d86eded283ec4f

**WinIoT on ラズパイでのI2C通信関連**
- [[C#/WinIoT/I2C] ラズパイ＋WindowsIoTCore＋C# で9軸センサ(MPU-9150)の値をとる](https://qiita.com/tera1707/items/33a8b439fddc1075f93b)

# やりたいこと・やったこと
電子工作で、ラズパイ3にWindows IoT Coreを入れて、サーマルカメラをつなげて、いわゆるサーモグラフィを作ってみたい。

さっと調べたところ、一番安くて(amazonで7000円くらい)手に入りやすそうな「MLX90640」を使おうと思うが、そのサーマルカメラがI2C接続のようなので、[以前、9軸センサで練習したI2C](https://qiita.com/tera1707/items/33a8b439fddc1075f93b)を生かせそう。

と思い立って作ったのが下記のようなもの。作るうえでいろいろ調べた事、やったことをメモしておく。

■動画
https://youtu.be/cPm-WGY2Y8c

■コード一式
https://github.com/tera1707/ThermalCamera

■写真(私の手をサーマル撮影)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/02436d38-4d7e-ef57-ba84-8e52dc261431.png)

# 使った機材

- RaspberryPi3
- [ラズパイ用小型モニタQuimat 3.5インチ](https://www.amazon.co.jp/gp/product/B075K56C12/ref=ppx_yo_dt_b_asin_title_o04_s00?ie=UTF8&psc=1)
- [サーマルカメラMLX90640](https://www.amazon.co.jp/gp/product/B07FF45134/ref=ppx_yo_dt_b_asin_title_o02_s00?ie=UTF8&psc=1)

# 全体の流れ

- 回路作成
- サーマルカメラとのI2C通信実装
 - VisualStudio2019ソリューションの設定
 - I2C通信のデータ送受信の準備
 - 初期化処理
 - EEPROM読み出し
 - サーマル生データを取得
 - 生データから温度に変換
 - 温度データからサーマル画像作成

# 回路作成
下図のような回路をつくる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/20d9a5c3-c681-18bb-15fd-84daee51f80a.png)
実際の配線
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/8153c707-53b0-8a5b-adf0-334da18e6c4a.png)
電源は、作成中、デバッグ中はコンセント～USBで。実際動かすときはモバイルバッテリーでの予定。

# サーマルカメラとのI2C通信実装
[以前実験した9軸センサのI2C](https://qiita.com/tera1707/items/33a8b439fddc1075f93b)をもとに、通信部分の実装を行う。

### VisualStudio2019ソリューションの設定
9軸センサのI2Cの方で同じ作業をしているので、そちら参照。

- [Windows IoT Extensions for UWPを参照に追加する](https://qiita.com/tera1707/items/33a8b439fddc1075f93b#%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E4%BD%9C%E6%88%90)
- [usingの追加](https://qiita.com/tera1707/items/33a8b439fddc1075f93b#%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E4%BD%9C%E6%88%90)

### I2C通信のデータ送受信の準備
[データシート](https://cdn.sparkfun.com/assets/7/b/f/2/d/MLX90640-Datasheet-Melexis.pdf)によると、このサーマルカメラは、2バイトを一単位としてデータのやり取りを行う。

9軸センサ(MPU-9150)の場合は、1バイト単位でデータをやり取りしていたので、そこが違う。
9軸センサの場合は、下記のようにしていた。

```9軸センサの送受信例.cs
// 書き込み：1バイト目に書き込みたいレジスタアドレス、2バイト目に書く内容を載せて送信
WriteBuf = new byte[] { 0x6B, 0x00 };
I2CAccel.Write(WriteBuf);

// 読み込み：1バイト目に読み込みたいレジスタアドレスを載せておくると、
// そのアドレスを先頭にした、指定バイト数分のデータが返ってくる
// (バイト数の指定は、ReadBufの配列数で行う(下記の場合はnew byte[1]なので1バイト))
WriteBuf = new byte[] { 0x75 };
ReadBuf = new byte[1];
I2CAccel.WriteRead(WriteBuf, ReadBuf);
```

今回のサーマルカメラでは、下記のようなメソッドを作って、ushortでアドレス指定、データ指定を行うようにした。

```サーマルカメラの送受信例.cs
// 書き込み
private void WriteRegisterData(ushort writeAddr, ushort data)
{
    // 書き込むデータ作成(最初の2バイトが書き込み先アドレス、その後の2バイトがそこに書き込むデータ)
    var writeByteData = new byte[]
    {
        (byte)(writeAddr / 0x100),  (byte)(writeAddr % 0x100),      // 書き込み先アドレス
        (byte)(data / 0x100),       (byte)(data % 0x100),           // 書き込みデータ
    };

    // 書き込み実施
    I2CThermalCamera.Write(writeByteData);
}

// 読み込み
private ushort[] ReadRegisterData(ushort readAddr, int NumberOfData)
{
    // 返すデータ(受信したbyteデータをushortに直したもの)
    ushort[] ret = new ushort[NumberOfData];

    // アドレスを上位/下位に分解
    var destAddr = new byte[] { (byte)(readAddr / 0x100), (byte)(readAddr % 0x100) };
    // 受信用バッファを確保(このサーマルカメラのレジスタは1つで2バイト)
    var readBuf = new byte[NumberOfData * 2];

    // 読み込み実施
    I2CThermalCamera.WriteRead(destAddr, readBuf);

    // 読み込んだbyteデータをushortに直す
    for (int i = 0; i < NumberOfData; i++)
    {
        ret[i] = (ushort)(readBuf[2 * i] * 0x100 + readBuf[2 * i + 1]);
    }
    return ret;
}
```

### 初期化処理
下記のような流れで初期化を行う。
(というか、`I2cDevice.FromIdAsync();`まではWinIotのI2C通信の準備)

```初期化処理時の通信.cs
public async Task InitThermalCamera()
{
    // すべてのI2Cデバイスを取得するためのセレクタ文字列を取得
    string aqs = I2cDevice.GetDeviceSelector(I2cDeviceName);
    DeviceInformationCollection dis = null;

    try
    {
        // セレクタ文字列を使ってI2Cコントローラデバイスを取得
        dis = await DeviceInformation.FindAllAsync(aqs);

        if (dis.Count == 0)
        {
            Debug.WriteLine("I2Cコントローラデバイスが見つかりませんでした");
            return;
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        throw;
    }

    // I2Cアドレスを指定して、デフォルトのI2C設定を作成する
    var settings = new I2cConnectionSettings(ThermalCameraI2CAddress);

    // バス速度を設定(FastMode：400 kHz)(指定しないと、標準設定(StandardMode：100kHz)になる)
    settings.BusSpeed = I2cBusSpeed.FastMode;

    // 取得したI2Cデバイスと作成した設定で、I2cDeviceのインスタンスを作成
    I2CThermalCamera = await I2cDevice.FromIdAsync(dis[0].Id, settings);

    if (I2CThermalCamera == null)
    {
        Debug.WriteLine(string.Format("スレーブアドレス {0} の I2C コントローラー {1} はほかのアプリで使用されています。他のアプリで使用されていないか、確認してください。", settings.SlaveAddress, dis[0].Id));
        return;
    }

    // サーマルカメラの設定
    try
    {
        // コントロールレジスタを取得
        var ctrreg = ReadRegisterData(0x800D, 1).FirstOrDefault();

        // リフレッシュレートを変更する(現在のコントロールレジスタを読み出して、そいつに対して変更を実施)
        var ctrregset = (ushort)(ctrreg | 0x0300);
        WriteRegisterData(0x800D, ctrregset);
    }
    catch (Exception ex)
    {
        Debug.WriteLine("デバイスとの通信に失敗しました。: " + ex.Message);
        return;
    }

    // EEPROM読み出し
    this.MLX90640_DumpEE();
}
```

ここで「リフレッシュレートを変更」しているが、今回は32Hzを使用した。(最速の64Hzにしなかったことに特に理由なし。)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/3330213d-d0c3-1580-c4e2-7f41c0d8b24e.png)

試したところ、リフレッシュレートの設定によって、下のデータ取得のところで出てくる「isReady」フラグ(0x8000のbit3)がONになるまでの時間が変わってくるので注意。


### ※以降の内容について
ここから下は、ほぼサンプルプログラムを基に作成しています。
⇒https://github.com/sparkfun/SparkFun_MLX90640_Arduino_Example

サンプルはC言語で描かれていますので、それをもとに、C#の処理を作成しました。

また、EEPROMの個別の中身や生データから温度に変換する計算など、データシートをじっくり読めばわかるのかもしれませんが、今回はあまり理解せずに(というか難しくてすぐに理解できなかった)サンプルを基に作らせて頂いてます。

### EEPROM読み出し
上の初期化の中の一番下、下記の部分。

```cs
// EEPROM読み出し
this.MLX90640_DumpEE();
```

EEPROMに保存されているパラメータが、読み出したサーマルデータの生データを温度の値に変換する際に使われるので、EEPROMから各種パラメータを読みだして、所定のクラスに格納しておく。(ここでは`ParamsMLX90640`クラスにいれた。)

データシートより、EEPROMはレジスタ0x2400番地から0x273F番地。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/900f7ed8-147a-03c1-09aa-3147a38f4155.png)

※EEPROMの読み出し項目は非常に多数あるので、コードは、githubの[こちら]()を参照。

### サーマル(生)データを取得 ～ 生データを温度データに変換
まずは、I2Cでサーマルの生データを取得する。
生データは温度データではないので、EEPROMから読み出したパラメータを使って温度に変換が必要。

データ取得は、データシートの「Measurement Flow」の項の流れに沿った処理を行う。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/9b72b990-93ab-d356-e66d-1142e247a29e.png)



```生データ読み出し.cs
public double[] GetTemperatureData()
{
    for (int i = 0; i < 2; i++)
    {
        byte isReady = 0;
        while (isReady == 0)
        {
            // ステータスレジスタ取得
            isReady = (byte)(ReadRegisterData(0x8000, 1).FirstOrDefault() & 0x0008);
        }

        //// ステータスレジスタ書き込み(MeasurementStartをON)
        WriteRegisterData(0x8000, 0x0030);

        // アIRデータ取得
        // 0x0400～0x06FF：IRデータ
        // 0x0700～0x070F：Ta_Vbe、CP.GAIN
        // 0x0720～Ta_PATA,CP,VddPix
        var frameDataS = ReadRegisterData(0x0400, FrameDataLength);

        // ステータスレジスタ読み出し(SubPage番号)
        //ReadRegisterData(0x8000, 1);
        StatusRegister = (ushort)(ReadRegisterData(0x8000, 1).FirstOrDefault() & 0x0001);

        // コントロールレジスタ読み出し
        ControlRegister = ReadRegisterData(0x800D, 1).FirstOrDefault();

        /////////////////////////
        // データ読み出し終了、データから温度への変換計算実施
        /////////////////////////
        var ta = this.MLX90640_GetTa(frameDataS, CamParameters);
        double tr = ta - 8;
        double[] ret = new double[FrameDataLength];

        // 生データを温度データに変換
        MLX90640_CalculateTo(frameDataS, CamParameters, 0.95, tr, ret);

        for (int l = 0; l < frameDataS.Length; l++)
        {
            if (ret[l] > 0.0)
            {
                TotalFrameData[l] = ret[l];
            }
        }
    }

    return TotalFrameData;
}
```

#### 生データ取得
メソッド`MLX90640_CalculateTo()`より上は、生データ取得処理。

生データを取る際、for分で2回回している。
データを採る際、「Subpage」という番号も一緒にとるのだが、これで全体のどの部分のデータが取れているかがわかる。
具体的には、データは下記の図のようなイメージで2回とって1画面分のデータとなる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/a38ac565-4f79-3031-02c1-8957514a8653.png)


ステータスレジスタの設定により2パターンの取れ方があるが、今回は上のパターン(横一行分のデータが1行飛ばしで取れるパターン)を使った。**つまり、subPageが0のデータは奇数行目のデータ、subPageが1のデータが1のデータは偶数行目のデータとなっている。**

#### 温度データに変換

そのsubPageの値と、取れてきた1行飛ばしのデータは、生データを温度データに変換するメソッド`MLX90640_CalculateTo()`の中で使っている。(中の計算ロジックは難しいのであまり見ず。)

変換した温度データを、最終的に配列に格納。(ここでは`TotalFrameData[]`)

**この配列の中身が、サーマルカメラに映った画面(32*24)の1ピクセルごとの温度の値となる。**

### 温度データからサーマル画像作成
温度データを画面に表示するための値にさらに変換する。
画面に温度を表す点を打つ方法は、[こちらの以前の記事](https://qiita.com/tera1707/items/1b265674b9dedbd1554c)を参照。

```温度の値を32*24のピクセルの描画データに変換する.cs
private async Task<BitmapImage> DoubleToRaindowColor(double[] totalFrameData)//temp:温度の値
{
    int width = 32;
    int height = 24;
    byte[] data = new byte[width * height * 4];

    for (int i = 0; i < width; i++)
    {
        for (int j = 0; j < height; j++)
        {
            // 指定の温度下限～上限の値を、0.0～1.0の値に変換する
            var v = TemperatureTo0to1Double(totalFrameData[i + j * width]);

            // 0.0～1.0の値を、虹色を表すバイト列に変換する
            var c = ColorScaleBCGYR(v);

            data[4 * (i + j * width)] = c.Item4;            // Blue
            data[4 * (i + j * width) + 1] = c.Item3;        // Green
            data[4 * (i + j * width) + 2] = c.Item2;        // Red
            data[4 * (i + j * width) + 3] = c.Item1;        // alpha
        }
    }

    // サーマル画像を作成
    WriteableBitmap bitmap = new WriteableBitmap(width, height);
    InMemoryRandomAccessStream inMRAS = new InMemoryRandomAccessStream();
    BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.BmpEncoderId, inMRAS);
    encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Ignore, (uint)bitmap.PixelWidth, (uint)bitmap.PixelHeight, 96.0, 96.0, data);
    await encoder.FlushAsync();
    BitmapImage bitmapImage = new BitmapImage();
    bitmapImage.SetSource(inMRAS);

    return bitmapImage;
}
```

上記の中の、指定の温度下限～上限の値を、0.0～1.0の値に変換するメソッド`ColorScaleBCGYR()`は、[こちらのサイト](https://qiita.com/krsak/items/94fad1d3fffa997cb651)を参考にさせていただいています。ありがとうございます。

# 完成
これで作成したbitmapを画面に表示したら、サーモグラフィの完成。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/02436d38-4d7e-ef57-ba84-8e52dc261431.png)

意外となめらかに動いているが、サーマルカメラの解像度が32*24と結構低いので、モザイク状に見える。最初、解像度、一桁間違えてないか？320*240の間違いでは？と思ったが、32*24であってた。
サーマルカメラは、割と解像度が低いものらしい。(高いものには、もっと高解像度のものもあるが、手が出ない)

# コード一式
ここまでで挙げてきた初期化やらデータ取得やらのメソッド以外にも、画面だったりC++のサンプルをもとに作ったEEPROM読み込み機能やらが多数ある。下記に一式置いているので参照ください。

https://github.com/tera1707/ThermalCamera

---
---
---

# ■191116 追記
コンセントから離れても動かせるよう、携帯の乾電池式充電器に接続。
あとサーマルカメラもブランブランならないよう、手抜きだが形だけユニバーサル基盤にくっつけて一旦のできあがり。
電気電子やらメカのプロの方からしたら怒られそうだが、個人的には手作り感満載で、落としたら一撃死しそうな感じがたまらなく良い。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/b4586bdf-8595-99c2-4b16-14441a2a64c7.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/94100259-af18-e7f0-0376-50d27e439c5c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/b58f13a1-fd38-bc57-a2bb-7951ca71803f.png)

↓↓↓敬礼する、メガネをかけた嫁
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352601/b62756fd-d428-acf1-8fc1-8dd93b37af1e.png)
※真ん中の四角の中の、3*3マスの平均温度を画面下に表示しているのだが、
　体温が異常に低く見えてる。放射率？とかパラメータ調整必要なのかも。

# 参考
公式ページ
https://shop.pimoroni.com/products/mlx90640-thermal-camera-breakout

データシート
https://cdn.sparkfun.com/assets/7/b/f/2/d/MLX90640-Datasheet-Melexis.pdf

値の大きさをサーモグラフィのような色に変換する
https://qiita.com/krsak/items/94fad1d3fffa997cb651

サンプルプログラム(arduino)
https://github.com/sparkfun/SparkFun_MLX90640_Arduino_Example/tree/master/Firmware

通信手順(どういうデータが取れるか、とか通信のお作法の参考になる)
https://github.com/sparkfun/SparkFun_MLX90640_Arduino_Example/blob/master/Firmware/Example1_BasicReadings/MLX90640_API.cpp

通信ドライバ(データの送り方の参考になる)
https://github.com/sparkfun/SparkFun_MLX90640_Arduino_Example/blob/master/Firmware/Example1_BasicReadings/MLX90640_I2C_Driver.cpp